@capybara-hq/plugin-model-fake 0.1.0-rc.0 → 0.1.0-rc.10
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.
- package/.capybara-plugin/plugin.json +147 -3
- package/dist/index.mjs +166 -40
- package/package.json +1 -1
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"name": "model-fake",
|
|
4
|
-
"version": "0.1.0-rc.
|
|
4
|
+
"version": "0.1.0-rc.10",
|
|
5
5
|
"publisher": "Capybara",
|
|
6
|
-
"description":
|
|
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.
|
|
19
|
+
"version": "0.1.0-rc.10",
|
|
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 {
|
|
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
|
-
|
|
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;
|
|
@@ -170,6 +178,20 @@ function scripted(req) {
|
|
|
170
178
|
error: new ModelError(kind, `Scripted ${kind} failure`)
|
|
171
179
|
};
|
|
172
180
|
}
|
|
181
|
+
const multi = /^tools:(\[[\s\S]*\])$/.exec(text);
|
|
182
|
+
if (multi) {
|
|
183
|
+
try {
|
|
184
|
+
const calls = JSON.parse(multi[1]);
|
|
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 };
|
|
188
|
+
} catch {
|
|
189
|
+
return {
|
|
190
|
+
kind: "fail",
|
|
191
|
+
error: new ModelError("invalid_request", "tools directive carries invalid JSON")
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
173
195
|
const tool = /^tool:([^:]+):([\s\S]*)$/.exec(text);
|
|
174
196
|
if (tool) {
|
|
175
197
|
try {
|
|
@@ -187,29 +209,72 @@ function scripted(req) {
|
|
|
187
209
|
return { kind: "text", text: words.join(" "), chunk: 64, delayMs: 0 };
|
|
188
210
|
}
|
|
189
211
|
if (hasToolResult) return std(`Tool result: ${result}`);
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
212
|
+
const only = req.tools.length === 1 ? req.tools[0] : void 0;
|
|
213
|
+
if (only !== void 0) return { kind: "tool", name: only.name, input: {} };
|
|
192
214
|
return std(`You said: ${text}`);
|
|
193
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
|
+
}
|
|
194
226
|
function createFakeProvider(opts) {
|
|
195
227
|
const sleep = opts.sleep ?? defaultSleep;
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
+
}
|
|
213
278
|
async function cacheUsage(req) {
|
|
214
279
|
const cached = req.system.filter((s) => s.cache).map((s) => s.text);
|
|
215
280
|
const prefix = cached.join("\n");
|
|
@@ -223,11 +288,34 @@ function createFakeProvider(opts) {
|
|
|
223
288
|
return { read: 0, write: tokens(cachedChars), cachedChars };
|
|
224
289
|
}
|
|
225
290
|
return {
|
|
226
|
-
async listModels() {
|
|
227
|
-
return
|
|
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
|
+
};
|
|
228
315
|
},
|
|
229
|
-
async *stream(req,
|
|
230
|
-
|
|
316
|
+
async *stream(req, call) {
|
|
317
|
+
const { signal } = call;
|
|
318
|
+
if (!infosFor(call).some((i) => i.id === req.model && (i.kind ?? "chat") === "chat")) {
|
|
231
319
|
throw new ModelError("invalid_request", `Unknown model "${req.model}"`);
|
|
232
320
|
}
|
|
233
321
|
if (signal.aborted) throw abortError();
|
|
@@ -242,13 +330,22 @@ function createFakeProvider(opts) {
|
|
|
242
330
|
cacheWriteTokens: cache.write
|
|
243
331
|
});
|
|
244
332
|
if (p.kind === "tool") {
|
|
245
|
-
const
|
|
246
|
-
const content =
|
|
333
|
+
const calls = [{ name: p.name, input: p.input }, ...p.more ?? []];
|
|
334
|
+
const content = calls.map((c, i) => ({
|
|
335
|
+
type: "tool_call",
|
|
336
|
+
id: `call_${createHash("sha256").update(
|
|
337
|
+
`${req.messages.length}:${i === 0 ? "" : `${i}:`}${c.name}:${JSON.stringify(c.input)}`
|
|
338
|
+
).digest("hex").slice(0, 12)}`,
|
|
339
|
+
name: c.name,
|
|
340
|
+
input: c.input
|
|
341
|
+
}));
|
|
247
342
|
yield {
|
|
248
343
|
type: "message_complete",
|
|
249
344
|
content,
|
|
250
345
|
stopReason: "tool_use",
|
|
251
|
-
usage: usage(
|
|
346
|
+
usage: usage(
|
|
347
|
+
calls.reduce((n, c) => n + c.name.length + JSON.stringify(c.input ?? null).length, 0)
|
|
348
|
+
)
|
|
252
349
|
};
|
|
253
350
|
return;
|
|
254
351
|
}
|
|
@@ -279,21 +376,37 @@ function createFakeProvider(opts) {
|
|
|
279
376
|
}
|
|
280
377
|
|
|
281
378
|
// src/index.ts
|
|
379
|
+
var label = (zh, en) => ({ ui: { label: { zh, en } } });
|
|
282
380
|
var DeploymentConfig = z.object({
|
|
283
|
-
models:
|
|
284
|
-
z.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
)
|
|
293
406
|
});
|
|
294
407
|
var index_default = defineExtension({
|
|
295
408
|
name: "model-fake",
|
|
296
|
-
version: "0.1.0-rc.
|
|
409
|
+
version: "0.1.0-rc.10",
|
|
297
410
|
sdk: "^0.1.0-rc.0",
|
|
298
411
|
provides: [MODEL_PROVIDER_EXTENSION_POINT_V2],
|
|
299
412
|
config: { deployment: DeploymentConfig },
|
|
@@ -301,7 +414,20 @@ var index_default = defineExtension({
|
|
|
301
414
|
const { models } = DeploymentConfig.parse(ctx.config ?? {});
|
|
302
415
|
ctx.provide(
|
|
303
416
|
MODEL_PROVIDER_EXTENSION_POINT_V2,
|
|
304
|
-
createFakeProvider({
|
|
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
|
+
})
|
|
305
431
|
);
|
|
306
432
|
}
|
|
307
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.
|
|
3
|
+
"version": "0.1.0-rc.10",
|
|
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",
|