@capybara-hq/plugin-model-fake 0.1.0-rc.1 → 0.1.0-rc.11

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.
@@ -1,15 +1,22 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "name": "model-fake",
4
- "version": "0.1.0-rc.1",
4
+ "version": "0.1.0-rc.11",
5
5
  "publisher": "Capybara",
6
- "description": "Deterministic scripted model provider (model-provider@2) for runtime tests, e2e tests and keyless demos",
6
+ "description": {
7
+ "en": "Deterministic scripted model provider (model-provider@2) for runtime tests, e2e tests and keyless demos",
8
+ "zh": "确定性的脚本化模型提供方(model-provider@2),用于运行时测试、端到端测试和无密钥演示"
9
+ },
10
+ "displayName": {
11
+ "zh": "模拟模型",
12
+ "en": "Fake model"
13
+ },
7
14
  "homepage": "https://github.com/capybara-hq/core/tree/main/plugins/model-fake#readme",
8
15
  "extension": {
9
16
  "entry": "./dist/index.mjs",
10
17
  "manifest": {
11
18
  "name": "model-fake",
12
- "version": "0.1.0-rc.1",
19
+ "version": "0.1.0-rc.11",
13
20
  "sdk": "^0.1.0-rc.0",
14
21
  "provides": [
15
22
  "model-provider@2"
@@ -27,6 +34,143 @@
27
34
  ],
28
35
  "org": []
29
36
  }
37
+ },
38
+ "configSchemas": {
39
+ "deployment": {
40
+ "type": "object",
41
+ "properties": {
42
+ "models": {
43
+ "type": "array",
44
+ "items": {
45
+ "type": "object",
46
+ "properties": {
47
+ "id": {
48
+ "type": "string",
49
+ "minLength": 1,
50
+ "x-ui": {
51
+ "label": {
52
+ "zh": "模型 ID",
53
+ "en": "Model ID"
54
+ }
55
+ },
56
+ "title": "Model ID"
57
+ },
58
+ "contextWindow": {
59
+ "type": "integer",
60
+ "exclusiveMinimum": 0,
61
+ "maximum": 9007199254740991,
62
+ "x-ui": {
63
+ "label": {
64
+ "zh": "上下文窗口",
65
+ "en": "Context window"
66
+ }
67
+ },
68
+ "title": "Context window"
69
+ },
70
+ "streamIdleTimeoutMs": {
71
+ "type": "integer",
72
+ "exclusiveMinimum": 0,
73
+ "maximum": 9007199254740991,
74
+ "x-ui": {
75
+ "label": {
76
+ "zh": "流空闲超时(毫秒)",
77
+ "en": "Stream idle timeout (ms)"
78
+ }
79
+ },
80
+ "title": "Stream idle timeout (ms)"
81
+ },
82
+ "vision": {
83
+ "type": "boolean",
84
+ "x-ui": {
85
+ "label": {
86
+ "zh": "图片输入",
87
+ "en": "Image input"
88
+ }
89
+ },
90
+ "title": "Image input"
91
+ },
92
+ "documents": {
93
+ "type": "boolean",
94
+ "x-ui": {
95
+ "label": {
96
+ "zh": "文档输入",
97
+ "en": "Document input"
98
+ }
99
+ },
100
+ "title": "Document input"
101
+ },
102
+ "kind": {
103
+ "type": "string",
104
+ "enum": [
105
+ "chat",
106
+ "embedding",
107
+ "rerank",
108
+ "image_generation",
109
+ "video_generation",
110
+ "speech_to_text",
111
+ "text_to_speech"
112
+ ],
113
+ "x-ui": {
114
+ "label": {
115
+ "zh": "类型",
116
+ "en": "Kind"
117
+ }
118
+ },
119
+ "title": "Kind"
120
+ },
121
+ "publisher": {
122
+ "type": "string",
123
+ "x-ui": {
124
+ "label": {
125
+ "zh": "发布者",
126
+ "en": "Publisher"
127
+ }
128
+ },
129
+ "title": "Publisher"
130
+ },
131
+ "dimensions": {
132
+ "type": "array",
133
+ "items": {
134
+ "type": "integer",
135
+ "exclusiveMinimum": 0,
136
+ "maximum": 9007199254740991
137
+ },
138
+ "x-ui": {
139
+ "label": {
140
+ "zh": "向量维度",
141
+ "en": "Embedding dimensions"
142
+ }
143
+ },
144
+ "title": "Embedding dimensions"
145
+ },
146
+ "maxBatch": {
147
+ "type": "integer",
148
+ "exclusiveMinimum": 0,
149
+ "maximum": 9007199254740991,
150
+ "x-ui": {
151
+ "label": {
152
+ "zh": "单次最大输入数",
153
+ "en": "Max inputs per call"
154
+ }
155
+ },
156
+ "title": "Max inputs per call"
157
+ }
158
+ },
159
+ "required": [
160
+ "id"
161
+ ]
162
+ },
163
+ "x-org": "override",
164
+ "x-ui": {
165
+ "label": {
166
+ "zh": "模型",
167
+ "en": "Models"
168
+ }
169
+ },
170
+ "title": "Models"
171
+ }
172
+ }
173
+ }
30
174
  }
31
175
  }
32
176
  }
package/dist/index.mjs CHANGED
@@ -6,7 +6,12 @@ const __capybara_filename = __capybaraFileURLToPath(import.meta.url);
6
6
  const __capybara_dirname = __capybaraPathDirname(__capybara_filename);
7
7
 
8
8
  // src/index.ts
9
- import { defineExtension, MODEL_PROVIDER_EXTENSION_POINT_V2 } from "@capybara-hq/plugin-sdk";
9
+ import {
10
+ defineExtension,
11
+ MODEL_PROVIDER_EXTENSION_POINT_V2,
12
+ ModelError as ModelError2,
13
+ orgAccess
14
+ } from "@capybara-hq/plugin-sdk";
10
15
  import { z } from "zod";
11
16
 
12
17
  // src/script.ts
@@ -104,6 +109,7 @@ function mediaSummary(req) {
104
109
  }
105
110
  return "none";
106
111
  }
112
+ var SENDER_LINE = /^From [^\n]+$/;
107
113
  function lastUser(req) {
108
114
  for (let i = req.messages.length - 1; i >= 0; i--) {
109
115
  const m = req.messages[i];
@@ -111,7 +117,9 @@ function lastUser(req) {
111
117
  let text = "";
112
118
  let result = "";
113
119
  let hasToolResult = false;
114
- for (const b of m.content) {
120
+ const first = m.content[0];
121
+ const blocks = m.content.length > 1 && first?.type === "text" && SENDER_LINE.test(first.text) ? m.content.slice(1) : m.content;
122
+ for (const b of blocks) {
115
123
  if (b.type === "text") text += b.text;
116
124
  else if (b.type === "tool_result") {
117
125
  hasToolResult = true;
@@ -174,9 +182,9 @@ function scripted(req) {
174
182
  if (multi) {
175
183
  try {
176
184
  const calls = JSON.parse(multi[1]);
177
- const [first2, ...more] = calls;
178
- if (first2 === void 0) throw new Error("empty");
179
- return { kind: "tool", name: first2.name, input: first2.input, more };
185
+ const [first, ...more] = calls;
186
+ if (first === void 0) throw new Error("empty");
187
+ return { kind: "tool", name: first.name, input: first.input, more };
180
188
  } catch {
181
189
  return {
182
190
  kind: "fail",
@@ -201,29 +209,72 @@ function scripted(req) {
201
209
  return { kind: "text", text: words.join(" "), chunk: 64, delayMs: 0 };
202
210
  }
203
211
  if (hasToolResult) return std(`Tool result: ${result}`);
204
- const first = req.tools[0];
205
- if (first !== void 0) return { kind: "tool", name: first.name, input: {} };
212
+ const only = req.tools.length === 1 ? req.tools[0] : void 0;
213
+ if (only !== void 0) return { kind: "tool", name: only.name, input: {} };
206
214
  return std(`You said: ${text}`);
207
215
  }
216
+ function embedVector(text, dim) {
217
+ const out = [];
218
+ for (let block = 0; out.length < dim; block++) {
219
+ const h = createHash("sha256").update(`${block}:${text}`).digest();
220
+ for (let i = 0; i + 1 < h.length && out.length < dim; i += 2) {
221
+ out.push(h.readUInt16BE(i) / 65535 * 2 - 1);
222
+ }
223
+ }
224
+ return out;
225
+ }
208
226
  function createFakeProvider(opts) {
209
227
  const sleep = opts.sleep ?? defaultSleep;
210
- const configured = opts.models !== void 0 && opts.models.length > 0 ? opts.models : [{ id: "fake-1" }];
211
- const infos = configured.map((m) => ({
212
- id: m.id,
213
- displayName: m.id,
214
- contextWindow: m.contextWindow ?? 32e3,
215
- maxOutputTokens: 4096,
216
- capabilities: {
217
- tools: true,
218
- vision: m.vision ?? false,
219
- documents: m.documents ?? false,
220
- reasoning: false,
221
- caching: true,
222
- midConversationSystem: true
223
- },
224
- ...m.streamIdleTimeoutMs !== void 0 ? { streamIdleTimeoutMs: m.streamIdleTimeoutMs } : {},
225
- parameters: parameters()
226
- }));
228
+ const ERROR_KINDS = [
229
+ "rate_limited",
230
+ "overloaded",
231
+ "server",
232
+ "timeout",
233
+ "network",
234
+ "auth",
235
+ "invalid_request",
236
+ "context_overflow",
237
+ "aborted"
238
+ ];
239
+ function infosOf(models) {
240
+ const configured = models !== void 0 && models.length > 0 ? models : [{ id: "fake-1" }];
241
+ return configured.map((m) => {
242
+ const embedding = m.kind === "embedding";
243
+ const dims = m.dimensions ?? [8, 16];
244
+ return {
245
+ id: m.id,
246
+ displayName: m.id,
247
+ ...m.kind !== void 0 ? { kind: m.kind } : {},
248
+ ...m.publisher !== void 0 ? { publisher: m.publisher } : {},
249
+ ...embedding ? {
250
+ embedding: {
251
+ dimensions: dims,
252
+ defaultDimensions: m.dimensions?.[0] ?? 8,
253
+ maxInputTokens: 512,
254
+ maxBatch: m.maxBatch ?? 4
255
+ }
256
+ } : {},
257
+ contextWindow: m.contextWindow ?? 32e3,
258
+ maxOutputTokens: 4096,
259
+ capabilities: {
260
+ tools: true,
261
+ vision: m.vision ?? false,
262
+ documents: m.documents ?? false,
263
+ reasoning: false,
264
+ caching: true,
265
+ midConversationSystem: true
266
+ },
267
+ ...m.streamIdleTimeoutMs !== void 0 ? { streamIdleTimeoutMs: m.streamIdleTimeoutMs } : {},
268
+ parameters: parameters()
269
+ };
270
+ });
271
+ }
272
+ const setupInfos = infosOf(opts.models);
273
+ function infosFor(call) {
274
+ if (call?.config === void 0 || opts.modelsOf === void 0) return setupInfos;
275
+ const models = opts.modelsOf(call.config);
276
+ return models === void 0 ? setupInfos : infosOf(models);
277
+ }
227
278
  async function cacheUsage(req) {
228
279
  const cached = req.system.filter((s) => s.cache).map((s) => s.text);
229
280
  const prefix = cached.join("\n");
@@ -237,11 +288,34 @@ function createFakeProvider(opts) {
237
288
  return { read: 0, write: tokens(cachedChars), cachedChars };
238
289
  }
239
290
  return {
240
- async listModels() {
241
- return infos.map((i) => structuredClone(i));
291
+ async listModels(call) {
292
+ return infosFor(call).map((i) => structuredClone(i));
293
+ },
294
+ async embed(req, call) {
295
+ if (call.signal.aborted) throw abortError();
296
+ const info = infosFor(call).find((i) => i.id === req.model && i.kind === "embedding");
297
+ if (info?.embedding === void 0) {
298
+ throw new ModelError("invalid_request", `Unknown embedding model "${req.model}"`);
299
+ }
300
+ const failure = /^fail:(\w+)/.exec(req.input[0] ?? "");
301
+ if (failure && ERROR_KINDS.includes(failure[1])) {
302
+ throw new ModelError(failure[1], `scripted failure: ${failure[1]}`);
303
+ }
304
+ const dim = req.dimensions ?? info.embedding.defaultDimensions;
305
+ if (!info.embedding.dimensions.includes(dim)) {
306
+ throw new ModelError("invalid_request", `Unsupported dimensions ${dim}`);
307
+ }
308
+ if (req.input.length > info.embedding.maxBatch) {
309
+ throw new ModelError("invalid_request", "Too many inputs in one embed call");
310
+ }
311
+ return {
312
+ vectors: req.input.map((t) => embedVector(t, dim)),
313
+ usage: { inputTokens: req.input.reduce((n, t) => n + Math.ceil(t.length / 4), 0) }
314
+ };
242
315
  },
243
- async *stream(req, { signal }) {
244
- if (!infos.some((i) => i.id === req.model)) {
316
+ async *stream(req, call) {
317
+ const { signal } = call;
318
+ if (!infosFor(call).some((i) => i.id === req.model && (i.kind ?? "chat") === "chat")) {
245
319
  throw new ModelError("invalid_request", `Unknown model "${req.model}"`);
246
320
  }
247
321
  if (signal.aborted) throw abortError();
@@ -302,21 +376,37 @@ function createFakeProvider(opts) {
302
376
  }
303
377
 
304
378
  // src/index.ts
379
+ var label = (zh, en) => ({ ui: { label: { zh, en } } });
305
380
  var DeploymentConfig = z.object({
306
- models: z.array(
307
- z.object({
308
- id: z.string().min(1),
309
- contextWindow: z.number().int().positive().optional(),
310
- streamIdleTimeoutMs: z.number().int().positive().optional(),
311
- /** 声明支持图片 / 文档输入(默认 false)。 */
312
- vision: z.boolean().optional(),
313
- documents: z.boolean().optional()
314
- })
315
- ).optional()
381
+ models: orgAccess(
382
+ z.array(
383
+ z.object({
384
+ id: z.string().min(1).meta(label("模型 ID", "Model ID")),
385
+ contextWindow: z.number().int().positive().optional().meta(label("上下文窗口", "Context window")),
386
+ streamIdleTimeoutMs: z.number().int().positive().optional().meta(label("流空闲超时(毫秒)", "Stream idle timeout (ms)")),
387
+ /** Declares image / document input support (default false). */
388
+ vision: z.boolean().optional().meta(label("图片输入", "Image input")),
389
+ documents: z.boolean().optional().meta(label("文档输入", "Document input")),
390
+ kind: z.enum([
391
+ "chat",
392
+ "embedding",
393
+ "rerank",
394
+ "image_generation",
395
+ "video_generation",
396
+ "speech_to_text",
397
+ "text_to_speech"
398
+ ]).optional().meta(label("类型", "Kind")),
399
+ publisher: z.string().optional().meta(label("发布者", "Publisher")),
400
+ dimensions: z.array(z.number().int().positive()).optional().meta(label("向量维度", "Embedding dimensions")),
401
+ maxBatch: z.number().int().positive().optional().meta(label("单次最大输入数", "Max inputs per call"))
402
+ })
403
+ ).optional().meta(label("模型", "Models")),
404
+ "override"
405
+ )
316
406
  });
317
407
  var index_default = defineExtension({
318
408
  name: "model-fake",
319
- version: "0.1.0-rc.1",
409
+ version: "0.1.0-rc.11",
320
410
  sdk: "^0.1.0-rc.0",
321
411
  provides: [MODEL_PROVIDER_EXTENSION_POINT_V2],
322
412
  config: { deployment: DeploymentConfig },
@@ -324,7 +414,20 @@ var index_default = defineExtension({
324
414
  const { models } = DeploymentConfig.parse(ctx.config ?? {});
325
415
  ctx.provide(
326
416
  MODEL_PROVIDER_EXTENSION_POINT_V2,
327
- createFakeProvider({ kv: ctx.kv, ...models !== void 0 ? { models } : {} })
417
+ createFakeProvider({
418
+ kv: ctx.kv,
419
+ ...models !== void 0 ? { models } : {},
420
+ modelsOf: (config) => {
421
+ const parsed = DeploymentConfig.safeParse(config ?? {});
422
+ if (!parsed.success) {
423
+ throw new ModelError2(
424
+ "invalid_request",
425
+ `Invalid model-fake config: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
426
+ );
427
+ }
428
+ return parsed.data.models;
429
+ }
430
+ })
328
431
  );
329
432
  }
330
433
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capybara-hq/plugin-model-fake",
3
- "version": "0.1.0-rc.1",
3
+ "version": "0.1.0-rc.11",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Deterministic scripted model provider (model-provider@2) for runtime tests, e2e tests and keyless demos",