@capybara-hq/plugin-model-fake 0.1.0-rc.2 → 0.1.0-rc.21

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,9 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "name": "model-fake",
4
- "version": "0.1.0-rc.2",
4
+ "version": "0.1.0-rc.21",
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
+ },
7
10
  "displayName": {
8
11
  "zh": "模拟模型",
9
12
  "en": "Fake model"
@@ -13,7 +16,7 @@
13
16
  "entry": "./dist/index.mjs",
14
17
  "manifest": {
15
18
  "name": "model-fake",
16
- "version": "0.1.0-rc.2",
19
+ "version": "0.1.0-rc.21",
17
20
  "sdk": "^0.1.0-rc.0",
18
21
  "provides": [
19
22
  "model-provider@2"
@@ -95,12 +98,69 @@
95
98
  }
96
99
  },
97
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"
98
157
  }
99
158
  },
100
159
  "required": [
101
160
  "id"
102
161
  ]
103
162
  },
163
+ "x-org": "override",
104
164
  "x-ui": {
105
165
  "label": {
106
166
  "zh": "模型",
package/dist/index.mjs CHANGED
@@ -8,7 +8,9 @@ const __capybara_dirname = __capybaraPathDirname(__capybara_filename);
8
8
  // src/index.ts
9
9
  import {
10
10
  defineExtension,
11
- MODEL_PROVIDER_EXTENSION_POINT_V2
11
+ MODEL_PROVIDER_EXTENSION_POINT_V2,
12
+ ModelError as ModelError2,
13
+ orgAccess
12
14
  } from "@capybara-hq/plugin-sdk";
13
15
  import { z } from "zod";
14
16
 
@@ -107,6 +109,7 @@ function mediaSummary(req) {
107
109
  }
108
110
  return "none";
109
111
  }
112
+ var SENDER_LINE = /^From [^\n]+$/;
110
113
  function lastUser(req) {
111
114
  for (let i = req.messages.length - 1; i >= 0; i--) {
112
115
  const m = req.messages[i];
@@ -114,7 +117,9 @@ function lastUser(req) {
114
117
  let text = "";
115
118
  let result = "";
116
119
  let hasToolResult = false;
117
- 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) {
118
123
  if (b.type === "text") text += b.text;
119
124
  else if (b.type === "tool_result") {
120
125
  hasToolResult = true;
@@ -177,9 +182,9 @@ function scripted(req) {
177
182
  if (multi) {
178
183
  try {
179
184
  const calls = JSON.parse(multi[1]);
180
- const [first2, ...more] = calls;
181
- if (first2 === void 0) throw new Error("empty");
182
- 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 };
183
188
  } catch {
184
189
  return {
185
190
  kind: "fail",
@@ -187,6 +192,22 @@ function scripted(req) {
187
192
  };
188
193
  }
189
194
  }
195
+ const slowTool = /^slowtool:(\d+):([^:]+):([\s\S]*)$/.exec(text);
196
+ if (slowTool) {
197
+ try {
198
+ return {
199
+ kind: "tool",
200
+ name: slowTool[2],
201
+ input: JSON.parse(slowTool[3]),
202
+ delayMs: Number(slowTool[1])
203
+ };
204
+ } catch {
205
+ return {
206
+ kind: "fail",
207
+ error: new ModelError("invalid_request", "slowtool directive carries invalid JSON")
208
+ };
209
+ }
210
+ }
190
211
  const tool = /^tool:([^:]+):([\s\S]*)$/.exec(text);
191
212
  if (tool) {
192
213
  try {
@@ -204,29 +225,72 @@ function scripted(req) {
204
225
  return { kind: "text", text: words.join(" "), chunk: 64, delayMs: 0 };
205
226
  }
206
227
  if (hasToolResult) return std(`Tool result: ${result}`);
207
- const first = req.tools[0];
208
- if (first !== void 0) return { kind: "tool", name: first.name, input: {} };
228
+ const only = req.tools.length === 1 ? req.tools[0] : void 0;
229
+ if (only !== void 0) return { kind: "tool", name: only.name, input: {} };
209
230
  return std(`You said: ${text}`);
210
231
  }
232
+ function embedVector(text, dim) {
233
+ const out = [];
234
+ for (let block = 0; out.length < dim; block++) {
235
+ const h = createHash("sha256").update(`${block}:${text}`).digest();
236
+ for (let i = 0; i + 1 < h.length && out.length < dim; i += 2) {
237
+ out.push(h.readUInt16BE(i) / 65535 * 2 - 1);
238
+ }
239
+ }
240
+ return out;
241
+ }
211
242
  function createFakeProvider(opts) {
212
243
  const sleep = opts.sleep ?? defaultSleep;
213
- const configured = opts.models !== void 0 && opts.models.length > 0 ? opts.models : [{ id: "fake-1" }];
214
- const infos = configured.map((m) => ({
215
- id: m.id,
216
- displayName: m.id,
217
- contextWindow: m.contextWindow ?? 32e3,
218
- maxOutputTokens: 4096,
219
- capabilities: {
220
- tools: true,
221
- vision: m.vision ?? false,
222
- documents: m.documents ?? false,
223
- reasoning: false,
224
- caching: true,
225
- midConversationSystem: true
226
- },
227
- ...m.streamIdleTimeoutMs !== void 0 ? { streamIdleTimeoutMs: m.streamIdleTimeoutMs } : {},
228
- parameters: parameters()
229
- }));
244
+ const ERROR_KINDS = [
245
+ "rate_limited",
246
+ "overloaded",
247
+ "server",
248
+ "timeout",
249
+ "network",
250
+ "auth",
251
+ "invalid_request",
252
+ "context_overflow",
253
+ "aborted"
254
+ ];
255
+ function infosOf(models) {
256
+ const configured = models !== void 0 && models.length > 0 ? models : [{ id: "fake-1" }];
257
+ return configured.map((m) => {
258
+ const embedding = m.kind === "embedding";
259
+ const dims = m.dimensions ?? [8, 16];
260
+ return {
261
+ id: m.id,
262
+ displayName: m.id,
263
+ ...m.kind !== void 0 ? { kind: m.kind } : {},
264
+ ...m.publisher !== void 0 ? { publisher: m.publisher } : {},
265
+ ...embedding ? {
266
+ embedding: {
267
+ dimensions: dims,
268
+ defaultDimensions: m.dimensions?.[0] ?? 8,
269
+ maxInputTokens: 512,
270
+ maxBatch: m.maxBatch ?? 4
271
+ }
272
+ } : {},
273
+ contextWindow: m.contextWindow ?? 32e3,
274
+ maxOutputTokens: 4096,
275
+ capabilities: {
276
+ tools: true,
277
+ vision: m.vision ?? false,
278
+ documents: m.documents ?? false,
279
+ reasoning: false,
280
+ caching: true,
281
+ midConversationSystem: true
282
+ },
283
+ ...m.streamIdleTimeoutMs !== void 0 ? { streamIdleTimeoutMs: m.streamIdleTimeoutMs } : {},
284
+ parameters: parameters()
285
+ };
286
+ });
287
+ }
288
+ const setupInfos = infosOf(opts.models);
289
+ function infosFor(call) {
290
+ if (call?.config === void 0 || opts.modelsOf === void 0) return setupInfos;
291
+ const models = opts.modelsOf(call.config);
292
+ return models === void 0 ? setupInfos : infosOf(models);
293
+ }
230
294
  async function cacheUsage(req) {
231
295
  const cached = req.system.filter((s) => s.cache).map((s) => s.text);
232
296
  const prefix = cached.join("\n");
@@ -240,11 +304,34 @@ function createFakeProvider(opts) {
240
304
  return { read: 0, write: tokens(cachedChars), cachedChars };
241
305
  }
242
306
  return {
243
- async listModels() {
244
- return infos.map((i) => structuredClone(i));
307
+ async listModels(call) {
308
+ return infosFor(call).map((i) => structuredClone(i));
309
+ },
310
+ async embed(req, call) {
311
+ if (call.signal.aborted) throw abortError();
312
+ const info = infosFor(call).find((i) => i.id === req.model && i.kind === "embedding");
313
+ if (info?.embedding === void 0) {
314
+ throw new ModelError("invalid_request", `Unknown embedding model "${req.model}"`);
315
+ }
316
+ const failure = /^fail:(\w+)/.exec(req.input[0] ?? "");
317
+ if (failure && ERROR_KINDS.includes(failure[1])) {
318
+ throw new ModelError(failure[1], `scripted failure: ${failure[1]}`);
319
+ }
320
+ const dim = req.dimensions ?? info.embedding.defaultDimensions;
321
+ if (!info.embedding.dimensions.includes(dim)) {
322
+ throw new ModelError("invalid_request", `Unsupported dimensions ${dim}`);
323
+ }
324
+ if (req.input.length > info.embedding.maxBatch) {
325
+ throw new ModelError("invalid_request", "Too many inputs in one embed call");
326
+ }
327
+ return {
328
+ vectors: req.input.map((t) => embedVector(t, dim)),
329
+ usage: { inputTokens: req.input.reduce((n, t) => n + Math.ceil(t.length / 4), 0) }
330
+ };
245
331
  },
246
- async *stream(req, { signal }) {
247
- if (!infos.some((i) => i.id === req.model)) {
332
+ async *stream(req, call) {
333
+ const { signal } = call;
334
+ if (!infosFor(call).some((i) => i.id === req.model && (i.kind ?? "chat") === "chat")) {
248
335
  throw new ModelError("invalid_request", `Unknown model "${req.model}"`);
249
336
  }
250
337
  if (signal.aborted) throw abortError();
@@ -259,6 +346,7 @@ function createFakeProvider(opts) {
259
346
  cacheWriteTokens: cache.write
260
347
  });
261
348
  if (p.kind === "tool") {
349
+ if (p.delayMs !== void 0 && p.delayMs > 0) await sleep(p.delayMs, signal);
262
350
  const calls = [{ name: p.name, input: p.input }, ...p.more ?? []];
263
351
  const content = calls.map((c, i) => ({
264
352
  type: "tool_call",
@@ -307,20 +395,35 @@ function createFakeProvider(opts) {
307
395
  // src/index.ts
308
396
  var label = (zh, en) => ({ ui: { label: { zh, en } } });
309
397
  var DeploymentConfig = z.object({
310
- models: z.array(
311
- z.object({
312
- id: z.string().min(1).meta(label("模型 ID", "Model ID")),
313
- contextWindow: z.number().int().positive().optional().meta(label("上下文窗口", "Context window")),
314
- streamIdleTimeoutMs: z.number().int().positive().optional().meta(label("流空闲超时(毫秒)", "Stream idle timeout (ms)")),
315
- /** Declares image / document input support (default false). */
316
- vision: z.boolean().optional().meta(label("图片输入", "Image input")),
317
- documents: z.boolean().optional().meta(label("文档输入", "Document input"))
318
- })
319
- ).optional().meta(label("模型", "Models"))
398
+ models: orgAccess(
399
+ z.array(
400
+ z.object({
401
+ id: z.string().min(1).meta(label("模型 ID", "Model ID")),
402
+ contextWindow: z.number().int().positive().optional().meta(label("上下文窗口", "Context window")),
403
+ streamIdleTimeoutMs: z.number().int().positive().optional().meta(label("流空闲超时(毫秒)", "Stream idle timeout (ms)")),
404
+ /** Declares image / document input support (default false). */
405
+ vision: z.boolean().optional().meta(label("图片输入", "Image input")),
406
+ documents: z.boolean().optional().meta(label("文档输入", "Document input")),
407
+ kind: z.enum([
408
+ "chat",
409
+ "embedding",
410
+ "rerank",
411
+ "image_generation",
412
+ "video_generation",
413
+ "speech_to_text",
414
+ "text_to_speech"
415
+ ]).optional().meta(label("类型", "Kind")),
416
+ publisher: z.string().optional().meta(label("发布者", "Publisher")),
417
+ dimensions: z.array(z.number().int().positive()).optional().meta(label("向量维度", "Embedding dimensions")),
418
+ maxBatch: z.number().int().positive().optional().meta(label("单次最大输入数", "Max inputs per call"))
419
+ })
420
+ ).optional().meta(label("模型", "Models")),
421
+ "override"
422
+ )
320
423
  });
321
424
  var index_default = defineExtension({
322
425
  name: "model-fake",
323
- version: "0.1.0-rc.2",
426
+ version: "0.1.0-rc.21",
324
427
  sdk: "^0.1.0-rc.0",
325
428
  provides: [MODEL_PROVIDER_EXTENSION_POINT_V2],
326
429
  config: { deployment: DeploymentConfig },
@@ -328,7 +431,20 @@ var index_default = defineExtension({
328
431
  const { models } = DeploymentConfig.parse(ctx.config ?? {});
329
432
  ctx.provide(
330
433
  MODEL_PROVIDER_EXTENSION_POINT_V2,
331
- createFakeProvider({ kv: ctx.kv, ...models !== void 0 ? { models } : {} })
434
+ createFakeProvider({
435
+ kv: ctx.kv,
436
+ ...models !== void 0 ? { models } : {},
437
+ modelsOf: (config) => {
438
+ const parsed = DeploymentConfig.safeParse(config ?? {});
439
+ if (!parsed.success) {
440
+ throw new ModelError2(
441
+ "invalid_request",
442
+ `Invalid model-fake config: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
443
+ );
444
+ }
445
+ return parsed.data.models;
446
+ }
447
+ })
332
448
  );
333
449
  }
334
450
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capybara-hq/plugin-model-fake",
3
- "version": "0.1.0-rc.2",
3
+ "version": "0.1.0-rc.21",
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",