@frockbot/plugin-provider-frock-ai 0.3.15 → 0.3.17

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/plugin-provider-frock-ai",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
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.15",
24
- "@frockbot/connection-core": "0.3.15",
25
- "@frockbot/kernel-agent-loop": "0.3.15",
26
- "@frockbot/kernel-contracts": "0.3.15",
27
- "@frockbot/plugin-models": "0.3.15",
28
- "@frockbot/provider-openai-compatible": "0.3.15",
23
+ "@frockbot/configuration-core": "0.3.17",
24
+ "@frockbot/connection-core": "0.3.17",
25
+ "@frockbot/kernel-agent-loop": "0.3.17",
26
+ "@frockbot/kernel-contracts": "0.3.17",
27
+ "@frockbot/plugin-models": "0.3.17",
28
+ "@frockbot/provider-openai-compatible": "0.3.17",
29
29
  "cordis": "4.0.0-rc.8"
30
30
  },
31
31
  "devDependencies": {
@@ -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",
@@ -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
- requestToWire,
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
- gatewayModelForFrockIdV1,
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(request: NormalizedModelRequest, signal: AbortSignal) {
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 wire = requestToWire(request);
159
- const { model: _model, ...body } = wire;
160
- const gatewayModel = gatewayModelForFrockIdV1(
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