Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a5ccfe6b79 |
@@ -69,6 +69,7 @@ jobs:
|
||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}
|
||||
MOONSHOTAI_SYNC_API_KEY: ${{ secrets.MOONSHOTAI_SYNC_API_KEY }}
|
||||
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
|
||||
CLOUDFLARE_WORKERS_AI_SYNC_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_WORKERS_AI_SYNC_ACCOUNT_ID }}
|
||||
CLOUDFLARE_WORKERS_AI_SYNC_API_TOKEN: ${{ secrets.CLOUDFLARE_WORKERS_AI_SYNC_API_TOKEN }}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AuthoredModel, AuthoredModelShape, ModelMetadata } from "../schema.js";
|
||||
import { baseten } from "./providers/baseten.js";
|
||||
import { cloudflareWorkersAi } from "./providers/cloudflare-workers-ai.js";
|
||||
import { google } from "./providers/google.js";
|
||||
import { moonshotai } from "./providers/moonshotai.js";
|
||||
import { openrouter } from "./providers/openrouter.js";
|
||||
import { ovhcloud } from "./providers/ovhcloud.js";
|
||||
import { vercel } from "./providers/vercel.js";
|
||||
@@ -80,6 +81,7 @@ export const providers: {
|
||||
baseten: SyncProvider<any>;
|
||||
"cloudflare-workers-ai": SyncProvider<any>;
|
||||
google: SyncProvider<any>;
|
||||
moonshotai: SyncProvider<any>;
|
||||
openrouter: SyncProvider<any>;
|
||||
ovhcloud: SyncProvider<any>;
|
||||
vercel: SyncProvider<any>;
|
||||
@@ -89,6 +91,7 @@ export const providers: {
|
||||
baseten,
|
||||
"cloudflare-workers-ai": cloudflareWorkersAi,
|
||||
google,
|
||||
moonshotai,
|
||||
openrouter,
|
||||
ovhcloud,
|
||||
vercel,
|
||||
@@ -99,7 +102,7 @@ export const providers: {
|
||||
export const groups = {
|
||||
aggregators: ["openrouter", "vercel"],
|
||||
cloudflare: ["cloudflare-workers-ai"],
|
||||
direct: ["baseten", "google", "ovhcloud", "venice", "xai"],
|
||||
direct: ["baseten", "google", "moonshotai", "ovhcloud", "venice", "xai"],
|
||||
} as const;
|
||||
|
||||
type ProviderID = keyof typeof providers;
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import type { ExistingModel, SyncProvider, SyncedFullModel, SyncedModel } from "../index.js";
|
||||
import { factorBaseModel } from "./openrouter.js";
|
||||
|
||||
const API_ENDPOINT = "https://api.moonshot.ai/v1/models";
|
||||
|
||||
const MoonshotModel = z.object({
|
||||
id: z.string().min(1),
|
||||
created: z.number().int().nonnegative(),
|
||||
context_length: z.number().int().positive(),
|
||||
supports_image_in: z.boolean().optional(),
|
||||
supports_video_in: z.boolean().optional(),
|
||||
supports_reasoning: z.boolean().optional(),
|
||||
}).passthrough();
|
||||
|
||||
export const MoonshotResponse = z.object({
|
||||
data: z.array(MoonshotModel),
|
||||
}).passthrough();
|
||||
|
||||
export type MoonshotModel = z.infer<typeof MoonshotModel>;
|
||||
|
||||
export const moonshotai = {
|
||||
id: "moonshotai",
|
||||
name: "Moonshot AI",
|
||||
modelsDir: "providers/moonshotai/models",
|
||||
skipCreates: true,
|
||||
deleteMissing: false,
|
||||
sourceID(model) {
|
||||
return model.id;
|
||||
},
|
||||
skippedNotice(ids) {
|
||||
if (ids.length === 0) return [];
|
||||
return [
|
||||
`${ids.length} Moonshot models returned by the API were not created because the Models API does not provide required pricing, output token limits, tool calling, structured output, or open-weight metadata. Existing models are still updated from API-authoritative fields.`,
|
||||
`Skipped remote IDs: ${ids.map((id) => `\`${id}\``).join(", ")}`,
|
||||
];
|
||||
},
|
||||
missingNotice(paths) {
|
||||
if (paths.length === 0) return [];
|
||||
return [
|
||||
`${paths.length} local Moonshot models were not returned by the API and were retained because the China provider references global model TOMLs through symlinks. Remove discontinued models from both providers in one reviewed change.`,
|
||||
`Retained local paths: ${paths.map((item) => `\`${item}\``).join(", ")}`,
|
||||
];
|
||||
},
|
||||
async fetchModels() {
|
||||
const key = process.env.MOONSHOTAI_SYNC_API_KEY;
|
||||
if (key === undefined) throw new Error("Moonshot AI sync requires MOONSHOTAI_SYNC_API_KEY");
|
||||
return fetchMoonshotModels(API_ENDPOINT, key);
|
||||
},
|
||||
parseModels(raw) {
|
||||
return MoonshotResponse.parse(raw).data;
|
||||
},
|
||||
translateModel(model, context) {
|
||||
const existing = context.existing(model.id);
|
||||
if (existing === undefined) return undefined;
|
||||
return { id: model.id, model: buildMoonshotModel(model, existing) };
|
||||
},
|
||||
} satisfies SyncProvider<MoonshotModel>;
|
||||
|
||||
export async function fetchMoonshotModels(
|
||||
endpoint: string,
|
||||
key: string,
|
||||
fetcher: typeof fetch = fetch,
|
||||
) {
|
||||
const response = await fetcher(endpoint, {
|
||||
headers: { Authorization: `Bearer ${key}` },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Moonshot models request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export function buildMoonshotModel(model: MoonshotModel, existing: ExistingModel): SyncedModel {
|
||||
const name = existing.name;
|
||||
const releaseDate = existing.release_date;
|
||||
const lastUpdated = existing.last_updated;
|
||||
const toolCall = existing.tool_call;
|
||||
const openWeights = existing.open_weights;
|
||||
const output = existing.limit?.output;
|
||||
const outputModalities = existing.modalities?.output;
|
||||
|
||||
if (
|
||||
name === undefined
|
||||
|| releaseDate === undefined
|
||||
|| lastUpdated === undefined
|
||||
|| toolCall === undefined
|
||||
|| openWeights === undefined
|
||||
|| output === undefined
|
||||
|| outputModalities === undefined
|
||||
) {
|
||||
throw new Error(`Moonshot model ${model.id} has incomplete local TOML metadata required for sync`);
|
||||
}
|
||||
|
||||
const input = [
|
||||
"text" as const,
|
||||
...(model.supports_image_in === true ? ["image" as const] : []),
|
||||
...(model.supports_video_in === true ? ["video" as const] : []),
|
||||
];
|
||||
const synced: SyncedFullModel = {
|
||||
name,
|
||||
family: existing.family,
|
||||
release_date: releaseDate,
|
||||
last_updated: lastUpdated,
|
||||
attachment: input.length > 1,
|
||||
reasoning: model.supports_reasoning === true,
|
||||
reasoning_options: model.supports_reasoning === true ? existing.reasoning_options : undefined,
|
||||
tool_call: toolCall,
|
||||
interleaved: model.supports_reasoning === true ? existing.interleaved : undefined,
|
||||
structured_output: existing.structured_output,
|
||||
temperature: existing.temperature,
|
||||
knowledge: existing.knowledge,
|
||||
open_weights: openWeights,
|
||||
status: existing.status,
|
||||
cost: existing.cost,
|
||||
limit: {
|
||||
context: model.context_length,
|
||||
input: existing.limit?.input,
|
||||
output,
|
||||
},
|
||||
modalities: { input, output: outputModalities },
|
||||
};
|
||||
|
||||
return existing.base_model === undefined
|
||||
? synced
|
||||
: factorBaseModel(existing.base_model, synced, synced.limit, existing.base_model_omit);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
import {
|
||||
buildMoonshotModel,
|
||||
fetchMoonshotModels,
|
||||
MoonshotResponse,
|
||||
type MoonshotModel,
|
||||
} from "../src/sync/providers/moonshotai.js";
|
||||
|
||||
const model: MoonshotModel = {
|
||||
id: "kimi-k2.7-code-highspeed",
|
||||
created: 1_781_516_706,
|
||||
context_length: 262_144,
|
||||
supports_image_in: true,
|
||||
supports_video_in: true,
|
||||
supports_reasoning: true,
|
||||
};
|
||||
|
||||
test("Moonshot sync keeps base models compact", () => {
|
||||
const synced = buildMoonshotModel(model, {
|
||||
base_model: "moonshotai/kimi-k2.7-code",
|
||||
name: "Kimi K2.7 Code HighSpeed",
|
||||
family: "kimi-k2",
|
||||
release_date: "2026-06-12",
|
||||
last_updated: "2026-06-12",
|
||||
attachment: true,
|
||||
reasoning: true,
|
||||
reasoning_options: [],
|
||||
tool_call: true,
|
||||
interleaved: { field: "reasoning_content" },
|
||||
structured_output: true,
|
||||
temperature: false,
|
||||
knowledge: "2025-01",
|
||||
open_weights: true,
|
||||
cost: { input: 1.9, output: 8, cache_read: 0.38 },
|
||||
limit: { context: 262_144, output: 262_144 },
|
||||
modalities: { input: ["text", "image", "video"], output: ["text"] },
|
||||
});
|
||||
|
||||
expect(synced).toEqual({
|
||||
base_model: "moonshotai/kimi-k2.7-code",
|
||||
name: "Kimi K2.7 Code HighSpeed",
|
||||
reasoning_options: [],
|
||||
interleaved: { field: "reasoning_content" },
|
||||
cost: { input: 1.9, output: 8, cache_read: 0.38 },
|
||||
});
|
||||
});
|
||||
|
||||
test("Moonshot sync maps authoritative capabilities and preserves curated fields", () => {
|
||||
const synced = buildMoonshotModel({
|
||||
...model,
|
||||
supports_image_in: undefined,
|
||||
supports_video_in: undefined,
|
||||
supports_reasoning: undefined,
|
||||
context_length: 131_072,
|
||||
}, {
|
||||
name: "Moonshot Test",
|
||||
family: "kimi-k2",
|
||||
release_date: "2025-01-01",
|
||||
last_updated: "2025-02-01",
|
||||
attachment: true,
|
||||
reasoning: true,
|
||||
reasoning_options: [{ type: "toggle" }],
|
||||
tool_call: true,
|
||||
interleaved: { field: "reasoning_content" },
|
||||
structured_output: true,
|
||||
temperature: true,
|
||||
open_weights: false,
|
||||
cost: { input: 1, output: 2 },
|
||||
limit: { context: 262_144, output: 32_768 },
|
||||
modalities: { input: ["text", "image"], output: ["text"] },
|
||||
});
|
||||
|
||||
expect(synced).toMatchObject({
|
||||
attachment: false,
|
||||
reasoning: false,
|
||||
tool_call: true,
|
||||
structured_output: true,
|
||||
cost: { input: 1, output: 2 },
|
||||
limit: { context: 131_072, output: 32_768 },
|
||||
modalities: { input: ["text"], output: ["text"] },
|
||||
});
|
||||
expect(synced.reasoning_options).toBeUndefined();
|
||||
expect(synced.interleaved).toBeUndefined();
|
||||
});
|
||||
|
||||
test("Moonshot rejects malformed catalog responses", () => {
|
||||
expect(() => MoonshotResponse.parse({ data: [{ id: "broken" }] })).toThrow();
|
||||
});
|
||||
|
||||
test("Moonshot rejects non-success API responses", async () => {
|
||||
const fetcher = async () => new Response("unauthorized", {
|
||||
status: 401,
|
||||
statusText: "Unauthorized",
|
||||
});
|
||||
|
||||
expect(fetchMoonshotModels("https://example.com/v1/models", "fixture-key", fetcher as typeof fetch))
|
||||
.rejects.toThrow("Moonshot models request failed: 401 Unauthorized");
|
||||
});
|
||||
@@ -14,6 +14,7 @@ The grouped sync targets are available for local convenience, but CI syncs each
|
||||
- `bun models:sync cloudflare` syncs the Cloudflare sync group.
|
||||
- `bun models:sync direct` syncs every provider in the `direct` group.
|
||||
- `bun models:sync google` syncs only Google.
|
||||
- `bun models:sync moonshotai` syncs only Moonshot AI.
|
||||
- `bun models:sync xai` syncs only xAI.
|
||||
- `bun models:sync aggregators --dry-run` prints changes without writing model files.
|
||||
- `bun models:sync aggregators --new-only` creates new model files but skips updates and removals.
|
||||
@@ -145,6 +146,15 @@ Google is implemented in `packages/core/src/sync/providers/google.ts`.
|
||||
- Local Google models missing from the API response are removed.
|
||||
- New Google API models are reported in `.sync/model-sync-report.md` but not created automatically because the API does not provide authoritative modalities, pricing, knowledge cutoff, release date, tool calling, or structured output metadata.
|
||||
|
||||
## Moonshot AI Notes
|
||||
|
||||
Moonshot AI is implemented in `packages/core/src/sync/providers/moonshotai.ts`.
|
||||
|
||||
- Source endpoint: `https://api.moonshot.ai/v1/models` with required `MOONSHOTAI_SYNC_API_KEY` auth.
|
||||
- The API is authoritative for context length, image/video input, and reasoning support on existing models.
|
||||
- New API models are reported but not created because the endpoint omits required pricing, output limits, tool calling, structured output, and open-weight metadata.
|
||||
- Missing local models are reported but retained because the China provider references global model TOMLs through symlinks.
|
||||
|
||||
## xAI Notes
|
||||
|
||||
xAI is implemented in `packages/core/src/sync/providers/xai.ts`.
|
||||
|
||||
Reference in New Issue
Block a user