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
54 changes: 54 additions & 0 deletions packages/workshop-backend/src/ai-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,21 @@ const API_STREAMS: Record<string, StreamFunction<Api, SimpleStreamOptions>> = {

const ZERO_COST: ModelCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };

// Per-model Moonshot billing (USD per 1k tokens), reasoning capability, and input modalities.
// Source: https://platform.moonshot.ai/docs/pricing/chat — verify against live page before shipping.
// Used because Moonshot isn't in pi-ai's built-in catalog, so catalogModel() returns undefined
// and we look up locally to avoid falling back to ZERO_COST (which would skew BYOK accounting).
const MOONSHOT_MODEL_COSTS: Record<string, {
cost: ModelCost,
reasoning: boolean,
input: ("text" | "image")[],
}> = {
"kimi-k3": { cost: { input: 0.002, output: 0.008, cacheRead: 0, cacheWrite: 0 }, reasoning: true, input: ["text"] },
"kimi-k2.7-code": { cost: { input: 0.0015, output: 0.006, cacheRead: 0, cacheWrite: 0 }, reasoning: true, input: ["text"] },
"kimi-k2.7-code-highspeed": { cost: { input: 0.0015, output: 0.006, cacheRead: 0, cacheWrite: 0 }, reasoning: true, input: ["text"] },
"kimi-k2.6": { cost: { input: 0.0006, output: 0.002, cacheRead: 0, cacheWrite: 0 }, reasoning: false, input: ["text"] },
};

// Consult pi's builtin catalog for cost/compat metadata of a known model id. Unknown models are
// fine (synthesized with zero cost). Import per-provider, not providers/all.
function catalogModel(provider: AiModelConfig["provider"], modelId: string): Model<Api> | undefined {
Expand All @@ -124,6 +139,7 @@ function catalogModel(provider: AiModelConfig["provider"], modelId: string): Mod
case "google": return (GOOGLE_MODELS as Record<string, Model<Api>>)[modelId];
case "cloudflare": return (CLOUDFLARE_WORKERS_AI_MODELS as Record<string, Model<Api>>)[modelId];
case "ollama": return undefined;
case "moonshot": return undefined; // Moonshot is OpenAI-compatible; pi doesn't ship a catalog.
default: return undefined;
}
}
Expand Down Expand Up @@ -231,6 +247,22 @@ function gatewayNativeModel(config: AiModelConfig, gatewayUrl: string): Model<Ap
...window,
compat: workersAiCompat(catalog),
};
case "moonshot":
// Moonshot is OpenAI-compatible; route through the gateway's /compat layer when the
// gateway is configured. Currently inactive for this workshop (no moonshot provider in
// the AI Gateway catalog), but kept symmetrical with `getModelDirect` so the direct
// path mirrors the gateway-routed one.
return {
id: config.model,
name: catalog?.name ?? config.model,
api: "openai-completions",
provider: "moonshot",
baseUrl: `${gatewayUrl}/compat`,
reasoning: true,
input: ["text"],
cost: catalog?.cost ?? ZERO_COST,
...window,
};
default:
return undefined;
}
Expand Down Expand Up @@ -587,6 +619,28 @@ function getModelDirect(config: AiModelConfig, sessionAffinity?: string): ModelH
apiKey: config.apiToken,
sessionAffinity,
});
case "moonshot": {
// Direct Moonshot hit (no AI Gateway). Moonshot exposes an OpenAI-compatible
// /v1/chat/completions endpoint, so we route through pi's openai-completions API impl.
// Cost / reasoning / input are per-model: lookup locally so pi-ai's missing catalog
// (catalogModel returns undefined) doesn't fall back to ZERO_COST.
const spec = MOONSHOT_MODEL_COSTS[config.model];
return makeHandle({
model: {
id: config.model,
name: catalog?.name ?? config.model,
api: "openai-completions",
provider: "moonshot",
baseUrl: config.apiUrl ?? "https://api.moonshot.ai/v1",
reasoning: spec?.reasoning ?? true,
input: spec?.input ?? ["text"],
cost: spec?.cost ?? ZERO_COST,
...window,
},
apiKey: config.apiToken,
sessionAffinity,
});
}
default:
config.provider satisfies never;
throw new Error(`Unknown provider "${config.provider}".`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const ATTACHMENT_SUPPORT_BY_PROVIDER = {
google: isTextImageOrPdfMime,
cloudflare: isTextOrImageMime,
ollama: isTextOrImageMime,
moonshot: isTextOrImageMime,
} satisfies Record<AiModelProvider, (mimeType: string) => boolean>;

function sanitizeChatAttachmentMimeType(mimeType: string | undefined): string {
Expand Down
6 changes: 6 additions & 0 deletions packages/workshop-backend/src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ declare global {
// Minimum connected-account balance (USD) to proceed via BYOK. Defaults to
// MINIMUM_CLOUDFLARE_BALANCE.
MINIMUM_CLOUDFLARE_BALANCE?: string;

// Moonshot AI provider (Kimi). The workshop hits api.moonshot.ai directly because Moonshot
// is not a Cloudflare first-party provider (no AI Gateway route for it). These env vars
// configure direct-mode defaults; per-model configurations in the user DO override them.
MOONSHOT_API_KEY?: string;
MOONSHOT_API_URL?: string;
}
}
}
13 changes: 12 additions & 1 deletion packages/workshop-shared/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -917,7 +917,7 @@ export type CloudflareAccountOption = {
};

// Supported AI providers.
export type AiModelProvider = "openai" | "anthropic" | "google" | "cloudflare" | "ollama";
export type AiModelProvider = "openai" | "anthropic" | "google" | "cloudflare" | "ollama" | "moonshot";

// Information about the AI gateway configuration. Returned by `AuthenticatedApi.getAiConfig()`.
export type AiGatewayInfo = {
Expand Down Expand Up @@ -985,6 +985,17 @@ export const SUGGESTED_MODELS: Record<
},
"ollama": {
},
"moonshot": {
// Moonshot AI — Kimi models. Hosted at api.moonshot.ai via OpenAI-compatible
// /v1/chat/completions; the workshop hits it directly (bypassing AI Gateway since
// Moonshot is not a Cloudflare first-party provider). contextWindow values per
// https://platform.moonshot.ai/docs/pricing/chat (K3: 262144, K2.7-code: 262144,
// K2.6: 131072).
"kimi-k3": {name: "Kimi K3 (Moonshot)", contextWindow: 262144, outputLimit: 32768},
"kimi-k2.7-code": {name: "Kimi K2.7 Code (Moonshot)", contextWindow: 262144, outputLimit: 32768},
"kimi-k2.7-code-highspeed": {name: "Kimi K2.7 Code Highspeed (Moonshot)", contextWindow: 262144, outputLimit: 32768},
"kimi-k2.6": {name: "Kimi K2.6 (Moonshot)", contextWindow: 131072, outputLimit: 8192},
},
};

// Metadata about a workspace (one Overseer DO and everything in it). Includes everything needed
Expand Down
2 changes: 2 additions & 0 deletions run-dev-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ for (const gk of gatekeepers) {
// over HTTPS with tokens).
"CF_AI_GATEWAY", "CF_AI_GATEWAY_PROVIDERS", "CF_AI_GATEWAY_ACCOUNT_ID",
"CF_AI_GATEWAY_API_TOKEN", "CF_AI_GATEWAY_WAI", "CF_AI_GATEWAY_WAI_DIRECT",
// Moonshot AI provider (Kimi models). Direct OpenAI-compatible → api.moonshot.ai/v1.
"MOONSHOT_API_KEY", "MOONSHOT_API_URL",
];
// OAuth app credentials (GOOGLE_/GITHUB_/CLOUDFLARE_OAUTH_*) are NOT passed to the backend anymore;
// they are injected into the gatekeeper Workers (see SHARED_GATEKEEPER_CREDS below).
Expand Down
Loading