@frockbot/plugin-provider-frock-ai 0.3.16 → 0.3.18
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/package.json +7 -7
- package/src/catalog.test.ts +11 -0
- package/src/catalog.ts +15 -0
- package/src/runtime.test.ts +72 -0
- package/src/runtime.ts +19 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-provider-frock-ai",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.18",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@frockbot/configuration-core": "0.3.
|
|
24
|
-
"@frockbot/connection-core": "0.3.
|
|
25
|
-
"@frockbot/kernel-agent-loop": "0.3.
|
|
26
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
27
|
-
"@frockbot/plugin-models": "0.3.
|
|
28
|
-
"@frockbot/provider-openai-compatible": "0.3.
|
|
23
|
+
"@frockbot/configuration-core": "0.3.18",
|
|
24
|
+
"@frockbot/connection-core": "0.3.18",
|
|
25
|
+
"@frockbot/kernel-agent-loop": "0.3.18",
|
|
26
|
+
"@frockbot/kernel-contracts": "0.3.18",
|
|
27
|
+
"@frockbot/plugin-models": "0.3.18",
|
|
28
|
+
"@frockbot/provider-openai-compatible": "0.3.18",
|
|
29
29
|
"cordis": "4.0.0-rc.8"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
package/src/catalog.test.ts
CHANGED
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
frockAiStaticCatalogV1,
|
|
6
6
|
frockModelIdForCloudflareIdV1,
|
|
7
7
|
gatewayModelForFrockIdV1,
|
|
8
|
+
gatewayModelForFrockRequestV1,
|
|
9
|
+
FROCK_AI_STRUCTURED_MODEL,
|
|
8
10
|
normalizeFrockModelIdV1,
|
|
9
11
|
} from "./catalog.js";
|
|
10
12
|
|
|
@@ -43,6 +45,15 @@ describe("Frock AI catalog", () => {
|
|
|
43
45
|
).toBe("dynamic/production-auto");
|
|
44
46
|
});
|
|
45
47
|
|
|
48
|
+
test("pins Auto schema work to a Workers AI model that supports it", () => {
|
|
49
|
+
expect(gatewayModelForFrockRequestV1(FROCK_AI_DEFAULT_MODEL, true)).toBe(
|
|
50
|
+
FROCK_AI_STRUCTURED_MODEL,
|
|
51
|
+
);
|
|
52
|
+
expect(gatewayModelForFrockRequestV1(FROCK_AI_DEFAULT_MODEL, false)).toBe(
|
|
53
|
+
"dynamic/flock-auto",
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
46
57
|
test("rejects ids outside the Frock AI namespace", () => {
|
|
47
58
|
expect(() => gatewayModelForFrockIdV1("@cf/not/frock")).toThrow(
|
|
48
59
|
'must start with "@frock/"',
|
package/src/catalog.ts
CHANGED
|
@@ -17,6 +17,9 @@ export const FROCK_AI_DEFAULT_MODEL = "@frock/auto";
|
|
|
17
17
|
* named `flock-auto`; the value is the resource's name, not ours.
|
|
18
18
|
*/
|
|
19
19
|
export const FROCK_AI_DEFAULT_AUTO_ROUTE = "flock-auto";
|
|
20
|
+
/** Workers AI model selected when Auto must honor a JSON Schema request. */
|
|
21
|
+
export const FROCK_AI_STRUCTURED_MODEL =
|
|
22
|
+
"workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast";
|
|
20
23
|
|
|
21
24
|
/** The pre-rename model-id prefix. Bots bound before the rename still carry it. */
|
|
22
25
|
export const FROCK_AI_LEGACY_MODEL_PREFIX = "@flock/";
|
|
@@ -86,6 +89,18 @@ export function gatewayModelForFrockIdV1(
|
|
|
86
89
|
return `workers-ai/${cloudflareModelIdForFrockIdV1(id)}`;
|
|
87
90
|
}
|
|
88
91
|
|
|
92
|
+
/** Auto routes ordinary chat dynamically and pins schema work to a capable model. */
|
|
93
|
+
export function gatewayModelForFrockRequestV1(
|
|
94
|
+
input: string,
|
|
95
|
+
structured: boolean,
|
|
96
|
+
autoRoute = FROCK_AI_DEFAULT_AUTO_ROUTE,
|
|
97
|
+
): string {
|
|
98
|
+
const id = normalizeFrockModelIdV1(input);
|
|
99
|
+
return structured && id === FROCK_AI_DEFAULT_MODEL
|
|
100
|
+
? FROCK_AI_STRUCTURED_MODEL
|
|
101
|
+
: gatewayModelForFrockIdV1(id, autoRoute);
|
|
102
|
+
}
|
|
103
|
+
|
|
89
104
|
const STATIC_CATALOG: ConnectionModelCatalogV1 = {
|
|
90
105
|
schemaVersion: 1,
|
|
91
106
|
generation: "flock-ai-static-v1",
|
package/src/runtime.test.ts
CHANGED
|
@@ -294,6 +294,7 @@ describe("Frock AI runtime Contribution", () => {
|
|
|
294
294
|
sse(
|
|
295
295
|
'data: {"choices":[{"delta":{"content":"Working"}}]}\n\n' +
|
|
296
296
|
'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call-1","function":{"name":"weather","arguments":"{\\"city\\":\\"Sydney\\"}"}}]},"finish_reason":"tool_calls"}]}\n\n' +
|
|
297
|
+
'data: {"choices":[],"usage":{"input_tokens":18,"output_tokens":6,"input_tokens_details":{"cached_tokens":4},"output_tokens_details":{"reasoning_tokens":2}}}\n\n' +
|
|
297
298
|
"data: [DONE]\n\n",
|
|
298
299
|
),
|
|
299
300
|
);
|
|
@@ -314,6 +315,7 @@ describe("Frock AI runtime Contribution", () => {
|
|
|
314
315
|
gatewayModel: "dynamic/configured-auto",
|
|
315
316
|
body: {
|
|
316
317
|
stream: true,
|
|
318
|
+
stream_options: { include_usage: true },
|
|
317
319
|
messages: [
|
|
318
320
|
{ role: "system", content: "Be concise." },
|
|
319
321
|
{ role: "user", content: "hello" },
|
|
@@ -333,6 +335,15 @@ describe("Frock AI runtime Contribution", () => {
|
|
|
333
335
|
]);
|
|
334
336
|
expect(events).toEqual([
|
|
335
337
|
{ type: "text-delta", text: "Working" },
|
|
338
|
+
{
|
|
339
|
+
type: "usage",
|
|
340
|
+
usage: {
|
|
341
|
+
inputTokens: 18,
|
|
342
|
+
outputTokens: 6,
|
|
343
|
+
cachedInputTokens: 4,
|
|
344
|
+
reasoningTokens: 2,
|
|
345
|
+
},
|
|
346
|
+
},
|
|
336
347
|
{
|
|
337
348
|
type: "tool-call",
|
|
338
349
|
call: {
|
|
@@ -346,6 +357,67 @@ describe("Frock AI runtime Contribution", () => {
|
|
|
346
357
|
await root.fiber.dispose();
|
|
347
358
|
});
|
|
348
359
|
|
|
360
|
+
test("sends Auto schemas in Workers AI's direct non-stream shape", async () => {
|
|
361
|
+
let call:
|
|
362
|
+
{ gatewayModel: string; body: Record<string, unknown> } | undefined;
|
|
363
|
+
const root = new Context();
|
|
364
|
+
await root.plugin(LlmRegistry);
|
|
365
|
+
await root.plugin(
|
|
366
|
+
createFrockAiRuntimePlugin(
|
|
367
|
+
runtimeConfig((gatewayModel, body) => {
|
|
368
|
+
call = { gatewayModel, body };
|
|
369
|
+
return Promise.resolve(
|
|
370
|
+
new Response(
|
|
371
|
+
JSON.stringify({
|
|
372
|
+
choices: [
|
|
373
|
+
{
|
|
374
|
+
message: { content: '{"answer":"yes"}' },
|
|
375
|
+
finish_reason: "stop",
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
}),
|
|
379
|
+
).body!,
|
|
380
|
+
);
|
|
381
|
+
}),
|
|
382
|
+
),
|
|
383
|
+
);
|
|
384
|
+
const events = [];
|
|
385
|
+
for await (const event of root.llm.stream(
|
|
386
|
+
{
|
|
387
|
+
...request,
|
|
388
|
+
responseFormat: {
|
|
389
|
+
type: "json_schema",
|
|
390
|
+
name: "answer",
|
|
391
|
+
schema: {
|
|
392
|
+
type: "object",
|
|
393
|
+
properties: { answer: { type: "string" } },
|
|
394
|
+
required: ["answer"],
|
|
395
|
+
additionalProperties: false,
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
new AbortController().signal,
|
|
400
|
+
)) {
|
|
401
|
+
events.push(event);
|
|
402
|
+
}
|
|
403
|
+
expect(call?.gatewayModel).toContain("llama-3.3-70b-instruct-fp8-fast");
|
|
404
|
+
expect(call?.body.stream).toBe(false);
|
|
405
|
+
expect(call?.body.response_format).toEqual({
|
|
406
|
+
type: "json_schema",
|
|
407
|
+
json_schema: {
|
|
408
|
+
type: "object",
|
|
409
|
+
properties: { answer: { type: "string" } },
|
|
410
|
+
required: ["answer"],
|
|
411
|
+
additionalProperties: false,
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
expect(events).toEqual([
|
|
415
|
+
{ type: "text-delta", text: '{"answer":"yes"}' },
|
|
416
|
+
{ type: "finish", reason: "completed" },
|
|
417
|
+
]);
|
|
418
|
+
await root.fiber.dispose();
|
|
419
|
+
});
|
|
420
|
+
|
|
349
421
|
test("refuses a request outside its pinned Connection generation", async () => {
|
|
350
422
|
let calls = 0;
|
|
351
423
|
const root = new Context();
|
package/src/runtime.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
boundedModelProviderReasonV1,
|
|
3
3
|
type LlmProvider,
|
|
4
4
|
type LlmReconciliationCapability,
|
|
5
|
+
type LlmStreamEvent,
|
|
5
6
|
ModelProviderFailureError,
|
|
6
7
|
type ModelProviderFailureClassV1,
|
|
7
8
|
ModelRequestDeadlineError,
|
|
@@ -10,7 +11,7 @@ import {
|
|
|
10
11
|
import {
|
|
11
12
|
classifyOpenAICompatibleFailureV1,
|
|
12
13
|
type ModelRequestDeadlineOptionsV1,
|
|
13
|
-
|
|
14
|
+
planOpenAICompatibleRequestV1,
|
|
14
15
|
streamWithModelRequestDeadlinesV1,
|
|
15
16
|
} from "@frockbot/provider-openai-compatible";
|
|
16
17
|
import type { Agent } from "@frockbot/kernel-agent-loop/agent";
|
|
@@ -18,7 +19,8 @@ import type { Plugin } from "cordis";
|
|
|
18
19
|
import {
|
|
19
20
|
FROCK_AI_DEFAULT_MODEL,
|
|
20
21
|
FROCK_AI_PROVIDER_TYPE,
|
|
21
|
-
|
|
22
|
+
gatewayModelForFrockRequestV1,
|
|
23
|
+
normalizeFrockModelIdV1,
|
|
22
24
|
} from "./catalog.js";
|
|
23
25
|
|
|
24
26
|
export type OpenAICompatibleChatCompletionBodyV1 = Record<string, unknown>;
|
|
@@ -125,6 +127,7 @@ export interface FrockAiRuntimeConfig {
|
|
|
125
127
|
|
|
126
128
|
class FrockAiProvider implements LlmProvider {
|
|
127
129
|
readonly id = FROCK_AI_PROVIDER_TYPE;
|
|
130
|
+
readonly supports = { structuredOutput: "json_schema" } as const;
|
|
128
131
|
readonly autoFallbackFailures = new WeakSet<ModelProviderFailureError>();
|
|
129
132
|
|
|
130
133
|
/**
|
|
@@ -143,7 +146,10 @@ class FrockAiProvider implements LlmProvider {
|
|
|
143
146
|
|
|
144
147
|
constructor(private readonly config: FrockAiRuntimeConfig) {}
|
|
145
148
|
|
|
146
|
-
async *stream(
|
|
149
|
+
async *stream(
|
|
150
|
+
request: NormalizedModelRequest,
|
|
151
|
+
signal: AbortSignal,
|
|
152
|
+
): AsyncIterable<LlmStreamEvent> {
|
|
147
153
|
const binding = request.modelBinding;
|
|
148
154
|
if (
|
|
149
155
|
binding?.connectionId !== this.config.connectionId ||
|
|
@@ -155,10 +161,17 @@ class FrockAiProvider implements LlmProvider {
|
|
|
155
161
|
});
|
|
156
162
|
}
|
|
157
163
|
signal.throwIfAborted();
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
const
|
|
164
|
+
const auto =
|
|
165
|
+
normalizeFrockModelIdV1(request.model) === FROCK_AI_DEFAULT_MODEL;
|
|
166
|
+
const plan = planOpenAICompatibleRequestV1(request, {
|
|
167
|
+
structuredOutput: auto ? "json_schema" : "none",
|
|
168
|
+
responseFormatDialect: "workers-ai",
|
|
169
|
+
});
|
|
170
|
+
if (plan.note) yield { type: "response-format-note", note: plan.note };
|
|
171
|
+
const { model: _model, ...body } = plan.body;
|
|
172
|
+
const gatewayModel = gatewayModelForFrockRequestV1(
|
|
161
173
|
request.model,
|
|
174
|
+
request.responseFormat !== undefined,
|
|
162
175
|
this.config.autoRoute,
|
|
163
176
|
);
|
|
164
177
|
// A rejection here happened before a stream existed, so no provider effect
|