@infimind-next/ai-hive-mcp 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,296 @@
1
+ import { z } from "zod";
2
+ import { noticeSchema } from "./user-notices.js";
3
+ const routingMode = z.enum(["COST_FIRST", "SPEED_FIRST", "SUCCESS_FIRST"]);
4
+ const reasoningEffort = z.enum([
5
+ "none",
6
+ "minimal",
7
+ "low",
8
+ "medium",
9
+ "high",
10
+ "xhigh",
11
+ "max",
12
+ ]);
13
+ const routingReasoning = z
14
+ .object({
15
+ routingMode,
16
+ reasoningEfforts: z
17
+ .array(reasoningEffort)
18
+ .min(1)
19
+ .refine((levels) => new Set(levels).size === levels.length, {
20
+ message: "思考档位不能重复",
21
+ }),
22
+ defaultReasoningEffort: reasoningEffort.optional(),
23
+ })
24
+ .strict()
25
+ .superRefine((value, ctx) => {
26
+ if (value.defaultReasoningEffort !== undefined &&
27
+ !value.reasoningEfforts.includes(value.defaultReasoningEffort)) {
28
+ ctx.addIssue({
29
+ code: z.ZodIssueCode.custom,
30
+ path: ["defaultReasoningEffort"],
31
+ message: "默认思考档位不在可用档位中",
32
+ });
33
+ }
34
+ });
35
+ const version = z.union([z.literal(1), z.literal(2)]).optional();
36
+ export const modelSectionSchema = z.enum(["parameters", "pricing", "examples"]);
37
+ export const listModelsReadShape = {
38
+ schemaVersion: version.describe("2 为分页轻目录,不传 detail;1 为旧格式。"),
39
+ modelType: z.enum(["TEXT", "IMAGE", "VIDEO"]).optional(),
40
+ detail: z
41
+ .enum(["lite", "full"])
42
+ .optional()
43
+ .describe("仅旧格式使用(schemaVersion=1 或不传版本);不能与 schemaVersion=2 同传。"),
44
+ query: z.string().max(200).optional(),
45
+ routingMode: routingMode.optional(),
46
+ cursor: z.string().min(1).max(4096).optional(),
47
+ limit: z.number().int().min(1).max(50).optional(),
48
+ };
49
+ export const getModelReadShape = {
50
+ schemaVersion: version.describe("2 为分章节详情;1 为旧格式。"),
51
+ publicModelId: z
52
+ .string()
53
+ .min(1)
54
+ .refine((value) => value.trim().length > 0)
55
+ .describe("使用 list_models 返回或已确认的 publicModelId,不按模型名称拼接。"),
56
+ routingMode: routingMode
57
+ .optional()
58
+ .describe("未知可用线路时先省略,从详情返回的 routingModes 中选择。"),
59
+ sections: z.array(modelSectionSchema).max(3).optional(),
60
+ };
61
+ function clean(input) {
62
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
63
+ }
64
+ export function parseModelListInput(raw, defaultVersion = 2) {
65
+ const input = z.object(listModelsReadShape).strict().parse(raw);
66
+ const selected = input.schemaVersion ?? (input.detail !== undefined ? 1 : defaultVersion);
67
+ if (selected === 2 && input.detail !== undefined)
68
+ throw new Error("新版模型查询不能同时使用 detail,请选择查询版本");
69
+ if (selected === 1 &&
70
+ [input.query, input.routingMode, input.cursor, input.limit].some((value) => value !== undefined))
71
+ throw new Error("筛选与分页需要新版模型查询,请选择 schemaVersion=2");
72
+ return clean({
73
+ ...input,
74
+ ...(selected === 2 ? { schemaVersion: 2 } : {}),
75
+ });
76
+ }
77
+ export function parseModelDetailInput(raw, defaultVersion = 2) {
78
+ const input = z.object(getModelReadShape).strict().parse(raw);
79
+ const selected = input.schemaVersion ?? defaultVersion;
80
+ if (selected !== 2 && input.sections !== undefined)
81
+ throw new Error("分章节读取需要新版模型查询,请选择 schemaVersion=2");
82
+ if (input.sections && new Set(input.sections).size !== input.sections.length)
83
+ throw new Error("模型查询章节不能重复");
84
+ return clean({
85
+ ...input,
86
+ ...(selected === 2 ? { schemaVersion: 2 } : {}),
87
+ });
88
+ }
89
+ const fileConfig = {
90
+ enabled: z.boolean(),
91
+ maxFileSizeBytes: z.number().int().positive(),
92
+ supportedMimeTypes: z.array(z.string().min(1)).min(1),
93
+ };
94
+ const textConfig = z
95
+ .object({
96
+ outputsThinking: z.boolean(),
97
+ showsThinkingToggle: z.boolean(),
98
+ attachments: z
99
+ .object({
100
+ textExtraction: z
101
+ .object({
102
+ ...fileConfig,
103
+ maxFilesPerMessage: z.number().int().positive().max(5),
104
+ })
105
+ .strict(),
106
+ imageInput: z.object(fileConfig).strict(),
107
+ })
108
+ .strict(),
109
+ })
110
+ .strict();
111
+ // 独立发布的客户端只验证公开传输契约, 完整计费规则仍由服务端校验; 不重写规则或转换价格数值.
112
+ const billingRule = z
113
+ .object({
114
+ version: z.union([z.literal(1), z.literal(2), z.literal(3)]),
115
+ kind: z.enum(["TEXT_TOKEN", "IMAGE_GENERATION", "VIDEO_GENERATION"]),
116
+ rules: z.array(z.record(z.unknown())).optional(),
117
+ tiers: z.array(z.record(z.unknown())).optional(),
118
+ })
119
+ .passthrough()
120
+ .superRefine((value, ctx) => {
121
+ if (!(value.rules?.length || value.tiers?.length))
122
+ ctx.addIssue({
123
+ code: z.ZodIssueCode.custom,
124
+ message: "缺少完整价格规则",
125
+ });
126
+ const inspect = (node, path) => {
127
+ if (Array.isArray(node))
128
+ node.forEach((item, index) => inspect(item, [...path, index]));
129
+ else if (node && typeof node === "object")
130
+ for (const [key, item] of Object.entries(node)) {
131
+ if (key === "saleMicros" &&
132
+ (typeof item !== "string" || !/^\d+$/.test(item)))
133
+ ctx.addIssue({
134
+ code: z.ZodIssueCode.custom,
135
+ path: [...path, key],
136
+ message: "价格必须保留整数文本",
137
+ });
138
+ inspect(item, [...path, key]);
139
+ }
140
+ };
141
+ inspect(value, []);
142
+ });
143
+ const snapshot = z
144
+ .object({
145
+ publicModelId: z.string().min(1),
146
+ routingMode,
147
+ billingRuleSnapshot: billingRule,
148
+ })
149
+ .strict();
150
+ const baseModel = {
151
+ publicModelId: z
152
+ .string()
153
+ .min(1)
154
+ .refine((value) => value.trim().length > 0),
155
+ displayName: z.string(),
156
+ description: z.string(),
157
+ modelType: z.enum(["TEXT", "IMAGE", "VIDEO"]),
158
+ materialMode: z.enum(["standard", "certified"]),
159
+ routingModes: z.array(routingMode).min(1),
160
+ textConfig: textConfig.optional(),
161
+ reasoningLevels: z.array(routingReasoning).min(1).optional(),
162
+ };
163
+ const listModel = z
164
+ .object({
165
+ publicModelId: baseModel.publicModelId,
166
+ displayName: baseModel.displayName,
167
+ modelType: baseModel.modelType,
168
+ materialMode: baseModel.materialMode,
169
+ })
170
+ .strict();
171
+ const detailModel = z
172
+ .object({
173
+ ...baseModel,
174
+ pricingSnapshot: z.array(snapshot).min(1).optional(),
175
+ promptSchema: z.record(z.unknown()).optional(),
176
+ paramsSchema: z.record(z.unknown()).optional(),
177
+ mediaRequirements: z.array(z.record(z.unknown())).optional(),
178
+ examples: z
179
+ .array(z
180
+ .object({
181
+ kind: z.string(),
182
+ label: z.string(),
183
+ input: z.record(z.unknown()),
184
+ })
185
+ .strict())
186
+ .optional(),
187
+ })
188
+ .strict();
189
+ function modelIssues(value, pricing, ctx, path) {
190
+ if (value.modelType === "TEXT" && !value.textConfig)
191
+ ctx.addIssue({
192
+ code: z.ZodIssueCode.custom,
193
+ path: [...path, "textConfig"],
194
+ message: "缺少文字能力摘要",
195
+ });
196
+ if (new Set(value.routingModes).size !== value.routingModes.length)
197
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path, message: "线路重复" });
198
+ if (value.reasoningLevels !== undefined) {
199
+ const modes = new Set();
200
+ for (const [index, level] of value.reasoningLevels.entries()) {
201
+ if (value.modelType !== "TEXT" ||
202
+ !value.routingModes.includes(level.routingMode) ||
203
+ modes.has(level.routingMode)) {
204
+ ctx.addIssue({
205
+ code: z.ZodIssueCode.custom,
206
+ path: [...path, "reasoningLevels", index],
207
+ message: "思考档位与文字模型或线路不一致",
208
+ });
209
+ }
210
+ modes.add(level.routingMode);
211
+ }
212
+ }
213
+ if (!pricing)
214
+ return;
215
+ const snapshots = value.pricingSnapshot ?? [];
216
+ if (snapshots.length !== value.routingModes.length ||
217
+ new Set(snapshots.map((item) => item.routingMode)).size !==
218
+ snapshots.length ||
219
+ snapshots.some((item) => item.publicModelId !== value.publicModelId ||
220
+ !value.routingModes.includes(item.routingMode)))
221
+ ctx.addIssue({
222
+ code: z.ZodIssueCode.custom,
223
+ path: [...path, "pricingSnapshot"],
224
+ message: "价格快照与模型线路不匹配",
225
+ });
226
+ }
227
+ const listResponse = z
228
+ .object({
229
+ schemaVersion: z.literal(2),
230
+ models: z.array(listModel),
231
+ nextCursor: z.string().min(1).max(4096).nullable(),
232
+ notices: z.array(noticeSchema),
233
+ })
234
+ .strict();
235
+ const detailResponse = z
236
+ .object({
237
+ schemaVersion: z.literal(2),
238
+ model: detailModel,
239
+ includedSections: z.array(modelSectionSchema).max(3),
240
+ notices: z.array(noticeSchema),
241
+ })
242
+ .strict()
243
+ .superRefine((value, ctx) => {
244
+ const sections = new Set(value.includedSections);
245
+ if (sections.size !== value.includedSections.length)
246
+ ctx.addIssue({
247
+ code: z.ZodIssueCode.custom,
248
+ path: ["includedSections"],
249
+ message: "章节重复",
250
+ });
251
+ modelIssues(value.model, sections.has("pricing"), ctx, ["model"]);
252
+ const model = value.model;
253
+ if (sections.has("pricing") !== (model.pricingSnapshot !== undefined))
254
+ ctx.addIssue({
255
+ code: z.ZodIssueCode.custom,
256
+ path: ["model", "pricingSnapshot"],
257
+ message: "价格章节不完整",
258
+ });
259
+ if (sections.has("examples") !== (model.examples !== undefined))
260
+ ctx.addIssue({
261
+ code: z.ZodIssueCode.custom,
262
+ path: ["model", "examples"],
263
+ message: "示例章节不完整",
264
+ });
265
+ if (sections.has("parameters") &&
266
+ model.modelType !== "TEXT" &&
267
+ (!model.paramsSchema || !model.mediaRequirements))
268
+ ctx.addIssue({
269
+ code: z.ZodIssueCode.custom,
270
+ path: ["model"],
271
+ message: "参数章节不完整",
272
+ });
273
+ if (!sections.has("parameters") &&
274
+ [model.promptSchema, model.paramsSchema, model.mediaRequirements].some((item) => item !== undefined))
275
+ ctx.addIssue({
276
+ code: z.ZodIssueCode.custom,
277
+ path: ["model"],
278
+ message: "返回了未选择的参数章节",
279
+ });
280
+ });
281
+ const V2_RESPONSE_ERROR = "当前接入未返回完整的新版模型响应,请确认服务端已升级或显式选择旧版查询";
282
+ export function parseModelListResponseV2(value) {
283
+ const parsed = listResponse.safeParse(value);
284
+ if (!parsed.success)
285
+ throw new Error(V2_RESPONSE_ERROR);
286
+ return parsed.data;
287
+ }
288
+ export function parseModelDetailResponseV2(value, publicModelId, sections = ["parameters", "pricing"]) {
289
+ const parsed = detailResponse.safeParse(value);
290
+ if (!parsed.success ||
291
+ parsed.data.model.publicModelId !== publicModelId ||
292
+ parsed.data.includedSections.length !== sections.length ||
293
+ sections.some((section) => !parsed.data.includedSections.includes(section)))
294
+ throw new Error(V2_RESPONSE_ERROR);
295
+ return parsed.data;
296
+ }
@@ -0,0 +1 @@
1
+ export declare const MCP_PACKAGE_VERSION = "0.5.0";
@@ -0,0 +1 @@
1
+ export const MCP_PACKAGE_VERSION = "0.5.0";
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { AiHiveApiClient, AiHiveApiError } from "./api-client.js";
3
3
  import { StaticBearerTokenProvider } from "./auth-token-provider.js";
4
4
  import { SERVER_VERSION } from "./server.js";
5
+ import { parseModelListInput, parseModelDetailInput, parseModelListResponseV2, } from "./model-response.js";
5
6
  // 这些命令让没有 MCP 宿主的 Agent(纯 shell)也能完成选型、试跑和查账;
6
7
  // 走的是同一个 /mcp 门面, 因此 MCP 密钥即可用, 不需要再申请 API 密钥.
7
8
  export async function runRestCommand(config, dependencies) {
@@ -34,33 +35,42 @@ export async function runRestCommand(config, dependencies) {
34
35
  }
35
36
  }
36
37
  async function runModels(config, client, dependencies) {
37
- const result = await client.listModels({
38
+ const input = parseModelListInput({
38
39
  ...(config.modelType ? { modelType: config.modelType } : {}),
39
- detail: config.detail ?? "lite",
40
- });
40
+ detail: config.detail,
41
+ schemaVersion: config.schemaVersion,
42
+ query: config.query,
43
+ cursor: config.cursor,
44
+ limit: config.limit,
45
+ routingMode: config.routingMode,
46
+ }, 2);
47
+ const result = await client.listModels(input);
41
48
  if (config.json) {
42
49
  dependencies.stdout(JSON.stringify(result, null, 2));
43
50
  return 0;
44
51
  }
45
- const models = Array.isArray(result) ? result : [];
52
+ const page = input.schemaVersion === 2 ? parseModelListResponseV2(result) : undefined;
53
+ const models = page?.models ?? (Array.isArray(result) ? result : []);
46
54
  if (models.length === 0) {
55
+ if (page?.nextCursor) {
56
+ dependencies.stdout(`当前页没有匹配模型,继续读取:models --cursor ${JSON.stringify(page.nextCursor)}(保留原筛选条件)`);
57
+ return 0;
58
+ }
47
59
  dependencies.stderr("没有可用的模型,请到控制台确认模型状态。");
48
60
  return 1;
49
61
  }
50
- dependencies.stdout("模型 ID\t名称\t类型\t线路");
62
+ dependencies.stdout("模型 ID\t名称\t类型\t素材模式");
51
63
  for (const model of models) {
52
64
  dependencies.stdout([
53
65
  text(model.publicModelId),
54
66
  text(model.displayName),
55
67
  text(model.modelType),
56
- Array.isArray(model.routingModes)
57
- ? model.routingModes
58
- .filter((mode) => typeof mode === "string")
59
- .join(",")
60
- : "",
68
+ text("materialMode" in model ? model.materialMode : undefined),
61
69
  ].join("\t"));
62
70
  }
63
- dependencies.stdout("价格与参数用 models --json 或 model <模型 ID> 查看。");
71
+ dependencies.stdout("找到候选即可用 model <模型 ID> 读取参数与价格;已知 ID 可直接查询。");
72
+ if (page?.nextCursor)
73
+ dependencies.stdout(`还有下一页:models --cursor ${JSON.stringify(page.nextCursor)}(保留原筛选条件)`);
64
74
  return 0;
65
75
  }
66
76
  async function runModel(config, client, dependencies) {
@@ -68,8 +78,16 @@ async function runModel(config, client, dependencies) {
68
78
  dependencies.stderr("请提供模型 ID,例如:ai-hive-mcp model doubao-seed-1-6");
69
79
  return 1;
70
80
  }
81
+ const input = parseModelDetailInput({
82
+ publicModelId: config.modelId,
83
+ routingMode: config.routingMode,
84
+ schemaVersion: config.schemaVersion,
85
+ sections: config.sections,
86
+ }, 2);
71
87
  const result = await client.getModel(config.modelId, config.routingMode, {
72
88
  announcements: false,
89
+ schemaVersion: input.schemaVersion,
90
+ ...(input.sections !== undefined ? { sections: input.sections } : {}),
73
91
  });
74
92
  dependencies.stdout(JSON.stringify(result, null, 2));
75
93
  return 0;
@@ -86,9 +104,12 @@ async function runPrompt(config, client, dependencies) {
86
104
  }
87
105
  const routingMode = config.routingMode ?? "COST_FIRST";
88
106
  // /mcp/chat/text 的严格入参要求 pricingSnapshot, CLI 没有宿主代为补全, 必须自己取一次.
89
- const model = (await client.getModel(config.modelId, routingMode, {
107
+ const detail = (await client.getModel(config.modelId, routingMode, {
108
+ schemaVersion: 2,
90
109
  announcements: false,
110
+ purpose: "SNAPSHOT_RESOLUTION",
91
111
  }));
112
+ const model = detail.model;
92
113
  const pricingSnapshot = Array.isArray(model.pricingSnapshot)
93
114
  ? model.pricingSnapshot.find((snapshot) => typeof snapshot === "object" &&
94
115
  snapshot !== null &&
package/dist/schemas.d.ts CHANGED
@@ -19,6 +19,7 @@ export declare const mcpCreateMediaUploadOutputSchema: z.ZodObject<{
19
19
  }>;
20
20
  expiresAt: z.ZodString;
21
21
  }, "strict", z.ZodTypeAny, {
22
+ expiresAt: string;
22
23
  mediaId: string;
23
24
  mediaType: "IMAGE" | "VIDEO" | "AUDIO" | "DOCUMENT";
24
25
  mimeType: string;
@@ -28,8 +29,8 @@ export declare const mcpCreateMediaUploadOutputSchema: z.ZodObject<{
28
29
  method: "PUT";
29
30
  headers: Record<string, string>;
30
31
  };
31
- expiresAt: string;
32
32
  }, {
33
+ expiresAt: string;
33
34
  mediaId: string;
34
35
  mediaType: "IMAGE" | "VIDEO" | "AUDIO" | "DOCUMENT";
35
36
  mimeType: string;
@@ -39,7 +40,6 @@ export declare const mcpCreateMediaUploadOutputSchema: z.ZodObject<{
39
40
  method: "PUT";
40
41
  headers: Record<string, string>;
41
42
  };
42
- expiresAt: string;
43
43
  }>;
44
44
  export declare const mcpUploadMediaFromPathInputSchema: z.ZodObject<{
45
45
  path: z.ZodEffects<z.ZodString, string, string>;
@@ -55,12 +55,19 @@ export declare const mcpUploadMediaFromPathInputSchema: z.ZodObject<{
55
55
  contentType?: string | undefined;
56
56
  }>;
57
57
  export declare const listModelsShape: {
58
+ schemaVersion: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<1>, z.ZodLiteral<2>]>>;
58
59
  modelType: z.ZodOptional<z.ZodEnum<["TEXT", "IMAGE", "VIDEO"]>>;
59
60
  detail: z.ZodOptional<z.ZodEnum<["lite", "full"]>>;
61
+ query: z.ZodOptional<z.ZodString>;
62
+ routingMode: z.ZodOptional<z.ZodEnum<["COST_FIRST", "SPEED_FIRST", "SUCCESS_FIRST"]>>;
63
+ cursor: z.ZodOptional<z.ZodString>;
64
+ limit: z.ZodOptional<z.ZodNumber>;
60
65
  };
61
66
  export declare const getModelShape: {
62
- publicModelId: z.ZodString;
67
+ schemaVersion: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<1>, z.ZodLiteral<2>]>>;
68
+ publicModelId: z.ZodEffects<z.ZodString, string, string>;
63
69
  routingMode: z.ZodOptional<z.ZodEnum<["COST_FIRST", "SPEED_FIRST", "SUCCESS_FIRST"]>>;
70
+ sections: z.ZodOptional<z.ZodArray<z.ZodEnum<["parameters", "pricing", "examples"]>, "many">>;
64
71
  };
65
72
  export declare const chatTextShape: {
66
73
  publicModelId: z.ZodString;
@@ -183,36 +190,36 @@ export declare const listBillingRecordsInputSchema: z.ZodEffects<z.ZodObject<{
183
190
  invocationSource: z.ZodOptional<z.ZodEnum<["UNKNOWN", "WEB", "MCP", "WORKBUDDY", "QWENWORK", "DOUBAO_WORK", "MIAOSHOU", "OPENAPI"]>>;
184
191
  }, "strict", z.ZodTypeAny, {
185
192
  pageSize: number;
193
+ cursor?: string | undefined;
186
194
  publicModelId?: string | undefined;
187
195
  from?: string | undefined;
188
196
  to?: string | undefined;
189
197
  taskIds?: string[] | undefined;
190
198
  invocationSource?: "UNKNOWN" | "WEB" | "MCP" | "WORKBUDDY" | "QWENWORK" | "DOUBAO_WORK" | "MIAOSHOU" | "OPENAPI" | undefined;
191
- cursor?: string | undefined;
192
199
  }, {
200
+ cursor?: string | undefined;
193
201
  publicModelId?: string | undefined;
194
202
  pageSize?: number | undefined;
195
203
  from?: string | undefined;
196
204
  to?: string | undefined;
197
205
  taskIds?: string[] | undefined;
198
206
  invocationSource?: "UNKNOWN" | "WEB" | "MCP" | "WORKBUDDY" | "QWENWORK" | "DOUBAO_WORK" | "MIAOSHOU" | "OPENAPI" | undefined;
199
- cursor?: string | undefined;
200
207
  }>, {
201
208
  pageSize: number;
209
+ cursor?: string | undefined;
202
210
  publicModelId?: string | undefined;
203
211
  from?: string | undefined;
204
212
  to?: string | undefined;
205
213
  taskIds?: string[] | undefined;
206
214
  invocationSource?: "UNKNOWN" | "WEB" | "MCP" | "WORKBUDDY" | "QWENWORK" | "DOUBAO_WORK" | "MIAOSHOU" | "OPENAPI" | undefined;
207
- cursor?: string | undefined;
208
215
  }, {
216
+ cursor?: string | undefined;
209
217
  publicModelId?: string | undefined;
210
218
  pageSize?: number | undefined;
211
219
  from?: string | undefined;
212
220
  to?: string | undefined;
213
221
  taskIds?: string[] | undefined;
214
222
  invocationSource?: "UNKNOWN" | "WEB" | "MCP" | "WORKBUDDY" | "QWENWORK" | "DOUBAO_WORK" | "MIAOSHOU" | "OPENAPI" | undefined;
215
- cursor?: string | undefined;
216
223
  }>;
217
224
  export declare const getBillingRecordInputSchema: z.ZodObject<{
218
225
  recordId: z.ZodString;
package/dist/schemas.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { getModelReadShape, listModelsReadShape } from "./model-response.js";
2
3
  const nonBlankStringSchema = z
3
4
  .string()
4
5
  .refine((value) => value.trim().length > 0, {
@@ -70,14 +71,8 @@ const clientRequestIdSchema = z
70
71
  message: "Expected non-blank string",
71
72
  })
72
73
  .optional();
73
- export const listModelsShape = {
74
- modelType: z.enum(["TEXT", "IMAGE", "VIDEO"]).optional(),
75
- detail: z.enum(["lite", "full"]).optional(),
76
- };
77
- export const getModelShape = {
78
- publicModelId: z.string().min(1),
79
- routingMode: routingModeSchema.optional(),
80
- };
74
+ export const listModelsShape = listModelsReadShape;
75
+ export const getModelShape = getModelReadShape;
81
76
  export const chatTextShape = {
82
77
  publicModelId: z.string().min(1),
83
78
  routingMode: routingModeSchema,
package/dist/server.d.ts CHANGED
@@ -5,7 +5,7 @@ import { AiHiveApiClient } from "./api-client.js";
5
5
  import { type BearerTokenProvider } from "./auth-token-provider.js";
6
6
  import type { McpClientConfig } from "./config.js";
7
7
  import { WorkBuddyCredentialStore } from "./workbuddy-credentials.js";
8
- export declare const SERVER_VERSION = "0.4.2";
8
+ export declare const SERVER_VERSION = "0.5.0";
9
9
  declare function content(value: unknown): {
10
10
  content: {
11
11
  type: "text";
package/dist/server.js CHANGED
@@ -3,17 +3,19 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { billingQueryInputSchema, listBillingRecordsInputSchema, getBillingRecordInputSchema, billingQueryShape, listBillingRecordsShape, getBillingRecordShape, chatTextShape, generateImageShape, generateVideoShape, getGenerationTaskShape, getModelShape, listModelsShape, listGenerationTasksShape, } from "./schemas.js";
5
5
  import { z } from "zod";
6
+ import { MCP_PACKAGE_VERSION } from "./package-version.js";
7
+ import { parseModelListInput, parseModelDetailInput, } from "./model-response.js";
6
8
  import { AiHiveApiClient, AiHiveApiError } from "./api-client.js";
7
9
  import { normalizeApiEndpoint, StaticBearerTokenProvider, WorkBuddyTokenProvider, } from "./auth-token-provider.js";
8
10
  import { readMcpClientInfo } from "./client-info.js";
9
11
  import { createUpdateNotifier } from "./update-notifier.js";
10
12
  import { uploadMediaFromPath } from "./local-media.js";
11
13
  import { WorkBuddyOAuthClient } from "./workbuddy-oauth-client.js";
12
- export const SERVER_VERSION = "0.4.2";
14
+ export const SERVER_VERSION = MCP_PACKAGE_VERSION;
13
15
  const DEPRECATED_ATTACHMENT_IDS_MESSAGE = "attachmentIds 已废弃,请使用 messages[].mediaIds";
14
- const SNAPSHOT_PARSE_FAILURE_MESSAGE = "pricingSnapshot 格式不正确,应为 list_models 返回的快照对象,也可以省略该字段由客户端自动获取";
15
- const SNAPSHOT_ARRAY_MISMATCH_MESSAGE = "pricingSnapshot 数组中没有匹配当前模型和 routingMode 的快照,请传入 list_models 返回的对应快照对象,或省略该字段由客户端自动获取";
16
- const SNAPSHOT_IDENTITY_MISMATCH_MESSAGE = "pricingSnapshot 的 publicModelId 或 routingMode 与请求不一致,请传入 list_models 返回的对应快照对象,或省略该字段由客户端自动获取";
16
+ const SNAPSHOT_PARSE_FAILURE_MESSAGE = "pricingSnapshot 格式不正确,应为 get_model 返回的快照对象,也可以省略该字段由客户端自动获取";
17
+ const SNAPSHOT_ARRAY_MISMATCH_MESSAGE = "pricingSnapshot 数组中没有匹配当前模型和 routingMode 的快照,请传入 get_model 返回的对应快照对象,或省略该字段由客户端自动获取";
18
+ const SNAPSHOT_IDENTITY_MISMATCH_MESSAGE = "pricingSnapshot 的 publicModelId 或 routingMode 与请求不一致,请传入 get_model 返回的对应快照对象,或省略该字段由客户端自动获取";
17
19
  const MODEL_UNAVAILABLE_MESSAGE = "模型当前不可用或不支持该路由模式,请重新调用 list_models 获取最新模型信息";
18
20
  // 此文案与 @ai-hive/contracts 的 USER_INFO_TOOL_* 常量保持一致; 独立发布的客户端不引入运行依赖.
19
21
  const USER_INFO_TOOL_TITLE = "查看蜂巢账户信息";
@@ -49,7 +51,7 @@ function pickSnapshotForRequest(snapshots, publicModelId, routingMode) {
49
51
  }
50
52
  // LLM 回传 pricingSnapshot 时经常漏传或变形(数组/JSON 字符串/null), 由客户端代为归一化,
51
53
  // 保证进入付费 API 的请求一定携带服务端期望的单个快照对象.
52
- async function resolvePricingSnapshot(client, modelType, args) {
54
+ async function resolvePricingSnapshot(client, _modelType, args) {
53
55
  const requestArgs = { ...args };
54
56
  if (requestArgs.dryRun === false) {
55
57
  delete requestArgs.dryRun;
@@ -62,10 +64,12 @@ async function resolvePricingSnapshot(client, modelType, args) {
62
64
  return request;
63
65
  };
64
66
  const resolveCurrentSnapshot = async () => {
65
- const models = await client().listModels({ modelType }, { announcements: false });
66
- const model = Array.isArray(models)
67
- ? models.find((item) => isPlainObject(item) && item.publicModelId === args.publicModelId)
68
- : undefined;
67
+ const result = await client().getModel(String(args.publicModelId), args.routingMode, {
68
+ schemaVersion: 2,
69
+ announcements: false,
70
+ purpose: "SNAPSHOT_RESOLUTION",
71
+ });
72
+ const model = isPlainObject(result) ? result.model : undefined;
69
73
  const snapshots = isPlainObject(model) ? model.pricingSnapshot : undefined;
70
74
  const matched = Array.isArray(snapshots)
71
75
  ? pickSnapshotForRequest(snapshots, args.publicModelId, args.routingMode)
@@ -130,21 +134,29 @@ export function createToolDefinitions(client) {
130
134
  },
131
135
  {
132
136
  name: "list_models",
133
- title: "查询可用模型与价格",
134
- description: "按 TEXT、IMAGE 或 VIDEO 查询可用模型与价格,售价单位是蜂蜜。detail=lite 用于选型,detail=full 返回完整契约。",
137
+ title: "筛选可用模型目录",
138
+ description: "按 TEXT、IMAGE 或 VIDEO 筛选可用模型目录,售价单位是蜂蜜。新版客户端默认使用 V2 分页;Remote MCP 查询显式传 schemaVersion=2,且不传 detail。query 可筛选模型,nextCursor 用于续查;按返回的 publicModelId 调用 get_model 读取详情。旧格式使用 schemaVersion=1,可选 detail=lite 或 detail=full。",
135
139
  schema: listModelsShape,
136
140
  annotations: { readOnlyHint: false },
137
- handler: async (args) => content(await client().listModels(args, { announcements: true })),
141
+ handler: async (args) => content(await client().listModels(parseModelListInput(args, 2), {
142
+ announcements: true,
143
+ })),
138
144
  },
139
145
  {
140
146
  name: "get_model",
141
147
  title: "查询模型调用契约",
142
- description: "按 publicModelId 查询模型的参数结构、媒体要求、计费因素和调用示例。调用或生成前应以此结果为准;传 routingMode 可只取该线路的契约。",
148
+ description: "使用目录返回或已确认的 publicModelId 查询模型调用契约,不按模型名称拼接 ID。未知可用线路时先不传 routingMode,从返回的 routingModes 中选择。新版客户端默认读取参数和价格,sections 可选择 parameters、pricing、examples;schemaVersion=1 可读取旧格式。Remote MCP 新查询请显式传 schemaVersion=2。调用或生成前应以完整契约为准。价格单位是蜂蜜;saleMicros 为整数文本,1000000 微单位 = 1 蜂蜜,不标元、人民币或 ¥;保留计费单位与适用条件。",
143
149
  schema: getModelShape,
144
150
  annotations: { readOnlyHint: false },
145
151
  handler: async (args) => {
146
- const publicModelId = String(args.publicModelId);
147
- return content(await client().getModel(publicModelId, typeof args.routingMode === "string" ? args.routingMode : undefined, { announcements: true }));
152
+ const input = parseModelDetailInput(args, 2);
153
+ return content(await client().getModel(input.publicModelId, input.routingMode, {
154
+ announcements: true,
155
+ schemaVersion: input.schemaVersion,
156
+ ...(input.sections !== undefined
157
+ ? { sections: input.sections }
158
+ : {}),
159
+ }));
148
160
  },
149
161
  },
150
162
  {
@@ -374,7 +386,7 @@ export function buildServerInstructions(webOrigin) {
374
386
  const lines = [
375
387
  "工具结果中独立的活动或公告文本是面向用户的通知:用户未拒绝促销且输出格式允许时,在本次回复末尾简短转达活动或公告摘要、已有的截止时间和链接;积分充足也适用。同一会话中相同活动已展示时避免重复,用户询问充值或积分不足时展开档位。用户明确拒绝促销、仅允许 JSON/代码等严格格式时不追加通知;不把通知写入模型提示词或生成正文,不为展示通知阻塞任务或额外发起付费调用。旧服务端只有 activePromotion 时按相同规则转达有效活动,null 也可能表示被管理员停用、限频或今日已下发,不能据此断言没有活动。",
376
388
  "AI Hive 是统一的大模型调用平台。",
377
- "模型单价、消费和积分使用蜂蜜,充值实付金额使用元。优先转达 pricingDisplay、billing.display 和账户展示字段;旧 currency=CNY 仅为兼容编码,不作为展示币种。换算以 1000000 micros = 1 蜂蜜为准,不把单价当作总费用;费用未知不得当作 0 蜂蜜。",
389
+ "模型单价、消费和积分使用蜂蜜,充值实付金额使用元。V2 从 pricingSnapshot 读取完整公开计费规则,saleMicros 保持整数文本,1000000 微单位等于 1 蜂蜜,不能把微单位直接当作蜂蜜或人民币。旧结果优先转达 pricingDisplay,任务用 billing.display,账户用展示字段;旧 currency=CNY 仅为兼容编码,不作为展示币种。不把单价当总费用,费用未知不得当作 0 蜂蜜。",
378
390
  "正式付费调用传稳定 clientRequestId;仅传输中断、REQUEST_IN_PROGRESS 或刷新过期报价时复用原值和相同参数。修正积分、参数、媒体或已缓存失败后换新值;REQUEST_OUTCOME_UNKNOWN 停止自动重试并核查。",
379
391
  "对用户只说明结果、费用、必要限制和下一步。工具描述、校验细节、报价字段、重试机制和通知策略用于调用端执行,不向用户复述。",
380
392
  "dryRun 的 readyToGenerate=true 与 mediaValidation=VERIFIED 仅表示参数和媒体预检结果,未执行人像或内容审核,不保证生成成功。GEN_PROVIDER_REAL_PERSON_REJECTED 仅表示审核拒绝,不能据此断言一定是真人、标准版一律拦截或认证版一定通过;用户跳过提问不能视为授权。价格以当前报价和 pricingSnapshot 为准,不按历史倍数计算;积分相同不能单独证明未扣费。",
@@ -383,7 +395,7 @@ export function buildServerInstructions(webOrigin) {
383
395
  "可用 get_user_info 查询蜂蜜积分、官网地址和充值入口。按 get_user_info 返回的 rechargeGuide 说明账户支付方式,不主动介绍账户未拥有的额度。不能仅凭钱包积分为 0 拒绝调用。只有实际调用返回积分不足时,才引导用户打开 rechargeUrl 充值后重试。",
384
396
  "积分不足时将返回的 rechargeUrl 展示为可点击链接;充值完成后等待用户确认继续,再按 recoveryAction 恢复原任务,不得自动发起付费重试。保留原模型、参数和费用确认,已返回任务 ID 时只查询原任务。",
385
397
  "pricingSnapshot 支持对象、数组或 JSON 字符串;数组按 publicModelId 和 routingMode 唯一匹配,不能取第一项或猜测价格。缺少或无法匹配时先读取 get_model,使用当前匹配快照;费用变化后重新取得用户确认。",
386
- "先用 list_models(detail=lite) 选型,再用 get_model 读取所选模型契约。上传前可省略媒体做 dryRun:缺素材不等于参数错误,费用未知不得当作 0 蜂蜜或上限。用户接受方案后上传,再带真实媒体预检;同参数同费用不重复确认,费用或条件变化才再确认。正式调用原样回传最后一次预检的 pricingSnapshot,费用数值保留字符串;仅 materialQuoteId 可自动准备,不能据此省略 pricingSnapshot。",
398
+ '新版客户端的 list_models/get_model 默认使用 V2;Remote MCP 查询显式传 schemaVersion=2,list_models 不传 detail。例如 list_models({"schemaVersion":2,"modelType":"VIDEO","limit":20})。按类型或 query 筛选轻目录,找到候选即可 get_model;已知 ID 直接读取详情,仅需完整目录时继续 nextCursor。publicModelId 使用目录返回或已确认的 ID;未知可用线路时先不传 routingMode,从返回的 routingModes 中选择。get_model 默认读取 parameters/pricing,examples 按需查询,正式调用前取齐参数与价格。旧格式传 schemaVersion=1,list_models 可选 detail=lite/full。上传前可省略媒体做 dryRun:缺素材不等于参数错误,费用未知不得当作 0 蜂蜜或上限。用户接受方案后上传,再带真实媒体预检;同参数同费用不重复确认,费用或条件变化才再确认。正式调用原样回传最后一次预检的 pricingSnapshot,费用数值保留字符串;仅 materialQuoteId 可自动准备,不能据此省略 pricingSnapshot。',
387
399
  "充值提醒只依据本次返回的 activePromotion;有值时用 title、headline 和 promoTiers 转达本场「限时特惠 · 充值加赠」,实付金额与入账积分沿用服务端结果,使用「距本场结束」说明 endsAt,并提供 rechargeUrl 的「立即充值」入口。模型调用返回 null 时不追加活动提醒;get_user_info 返回 null 时不推断具体原因。缓存或幂等重放中的 endsAt 已过期时,先重新查询 get_user_info,以新结果为准,不沿用旧截止时间或推算后续活动。",
388
400
  "调用 chat_text、generate_image、generate_video 前,用 get_model 确认参数契约;正式调用必须传稳定的 clientRequestId 幂等键,修正参数后换新值。",
389
401
  "chat_text 同步返回,长推理模型单次可能耗时 1-3 分钟,客户端超时应设为 300 秒以上;等待超时只代表本端不再等待,服务端可能仍在生成,也可能已完成并扣费,超时后复用同一 clientRequestId 和相同业务参数重新调用即可取回首次结果,修正参数后换新值。",