@capybara-hq/plugin-model-fake 0.1.0-rc.3 → 0.1.0-rc.5
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 +59 -2
- package/dist/index.mjs +139 -40
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"name": "model-fake",
|
|
4
|
-
"version": "0.1.0-rc.
|
|
4
|
+
"version": "0.1.0-rc.5",
|
|
5
5
|
"publisher": "Capybara",
|
|
6
6
|
"description": {
|
|
7
7
|
"en": "Deterministic scripted model provider (model-provider@2) for runtime tests, e2e tests and keyless demos",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"entry": "./dist/index.mjs",
|
|
17
17
|
"manifest": {
|
|
18
18
|
"name": "model-fake",
|
|
19
|
-
"version": "0.1.0-rc.
|
|
19
|
+
"version": "0.1.0-rc.5",
|
|
20
20
|
"sdk": "^0.1.0-rc.0",
|
|
21
21
|
"provides": [
|
|
22
22
|
"model-provider@2"
|
|
@@ -98,12 +98,69 @@
|
|
|
98
98
|
}
|
|
99
99
|
},
|
|
100
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"
|
|
101
157
|
}
|
|
102
158
|
},
|
|
103
159
|
"required": [
|
|
104
160
|
"id"
|
|
105
161
|
]
|
|
106
162
|
},
|
|
163
|
+
"x-org": "override",
|
|
107
164
|
"x-ui": {
|
|
108
165
|
"label": {
|
|
109
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
|
-
|
|
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 [
|
|
181
|
-
if (
|
|
182
|
-
return { kind: "tool", name:
|
|
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",
|
|
@@ -204,29 +209,72 @@ function scripted(req) {
|
|
|
204
209
|
return { kind: "text", text: words.join(" "), chunk: 64, delayMs: 0 };
|
|
205
210
|
}
|
|
206
211
|
if (hasToolResult) return std(`Tool result: ${result}`);
|
|
207
|
-
const
|
|
208
|
-
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: {} };
|
|
209
214
|
return std(`You said: ${text}`);
|
|
210
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
|
+
}
|
|
211
226
|
function createFakeProvider(opts) {
|
|
212
227
|
const sleep = opts.sleep ?? defaultSleep;
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
+
}
|
|
230
278
|
async function cacheUsage(req) {
|
|
231
279
|
const cached = req.system.filter((s) => s.cache).map((s) => s.text);
|
|
232
280
|
const prefix = cached.join("\n");
|
|
@@ -240,11 +288,34 @@ function createFakeProvider(opts) {
|
|
|
240
288
|
return { read: 0, write: tokens(cachedChars), cachedChars };
|
|
241
289
|
}
|
|
242
290
|
return {
|
|
243
|
-
async listModels() {
|
|
244
|
-
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
|
+
};
|
|
245
315
|
},
|
|
246
|
-
async *stream(req,
|
|
247
|
-
|
|
316
|
+
async *stream(req, call) {
|
|
317
|
+
const { signal } = call;
|
|
318
|
+
if (!infosFor(call).some((i) => i.id === req.model && (i.kind ?? "chat") === "chat")) {
|
|
248
319
|
throw new ModelError("invalid_request", `Unknown model "${req.model}"`);
|
|
249
320
|
}
|
|
250
321
|
if (signal.aborted) throw abortError();
|
|
@@ -307,20 +378,35 @@ function createFakeProvider(opts) {
|
|
|
307
378
|
// src/index.ts
|
|
308
379
|
var label = (zh, en) => ({ ui: { label: { zh, en } } });
|
|
309
380
|
var DeploymentConfig = z.object({
|
|
310
|
-
models:
|
|
311
|
-
z.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+
)
|
|
320
406
|
});
|
|
321
407
|
var index_default = defineExtension({
|
|
322
408
|
name: "model-fake",
|
|
323
|
-
version: "0.1.0-rc.
|
|
409
|
+
version: "0.1.0-rc.5",
|
|
324
410
|
sdk: "^0.1.0-rc.0",
|
|
325
411
|
provides: [MODEL_PROVIDER_EXTENSION_POINT_V2],
|
|
326
412
|
config: { deployment: DeploymentConfig },
|
|
@@ -328,7 +414,20 @@ var index_default = defineExtension({
|
|
|
328
414
|
const { models } = DeploymentConfig.parse(ctx.config ?? {});
|
|
329
415
|
ctx.provide(
|
|
330
416
|
MODEL_PROVIDER_EXTENSION_POINT_V2,
|
|
331
|
-
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
|
+
})
|
|
332
431
|
);
|
|
333
432
|
}
|
|
334
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.5",
|
|
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",
|