@jaypie/llm 1.3.12 → 1.3.13

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.
@@ -106,6 +106,7 @@ type FireworksContentPart = {
106
106
  export declare class FireworksAdapter extends BaseProviderAdapter {
107
107
  readonly name: "fireworks";
108
108
  readonly defaultModel: string;
109
+ readonly supportsStructuredOutputRetry = true;
109
110
  private runtimeNoStructuredOutputModels;
110
111
  rememberModelRejectsStructuredOutput(model: string): void;
111
112
  clearRuntimeNoStructuredOutputModels(): void;
@@ -20,6 +20,12 @@ export interface ProviderAdapter {
20
20
  * Default model for this provider
21
21
  */
22
22
  readonly defaultModel: string;
23
+ /**
24
+ * Whether OperateLoop may take a corrective turn when a `format` request
25
+ * completes with prose instead of structured output (tool-emulation
26
+ * providers opt in).
27
+ */
28
+ readonly supportsStructuredOutputRetry?: boolean;
23
29
  /**
24
30
  * Build a provider-specific request from the standardized format
25
31
  *
@@ -169,6 +175,13 @@ export declare abstract class BaseProviderAdapter implements ProviderAdapter {
169
175
  abstract responseToHistoryItems(response: unknown): LlmHistory;
170
176
  abstract classifyError(error: unknown): ClassifiedError;
171
177
  abstract isComplete(response: unknown): boolean;
178
+ /**
179
+ * Whether OperateLoop may take a corrective turn when a `format` request
180
+ * completes with prose instead of structured output. Providers whose
181
+ * structured output rides a tool emulation (where compliance is a model
182
+ * decision) opt in; native grammar-constrained providers do not need it.
183
+ */
184
+ readonly supportsStructuredOutputRetry: boolean;
172
185
  /**
173
186
  * Default implementation checks if error is retryable via classifyError
174
187
  */
@@ -77,6 +77,13 @@ export interface OperateRequest {
77
77
  stream?: boolean;
78
78
  /** Sampling temperature (0-2 for most providers) */
79
79
  temperature?: number;
80
+ /**
81
+ * Set by OperateLoop on a corrective turn after a format request came back
82
+ * as prose. Adapters that support the retry (see
83
+ * `ProviderAdapter.supportsStructuredOutputRetry`) should offer only the
84
+ * structured-output mechanism on this turn.
85
+ */
86
+ structuredOutputRetry?: boolean;
80
87
  /** User identifier for tracking */
81
88
  user?: string;
82
89
  }
@@ -135,6 +142,8 @@ import type { ResponseBuilder } from "./response/ResponseBuilder.js";
135
142
  export interface OperateLoopState {
136
143
  /** Count of consecutive tool errors (resets on success) */
137
144
  consecutiveToolErrors: number;
145
+ /** Set when the loop has taken a corrective structured-output turn */
146
+ structuredOutputRetry?: boolean;
138
147
  /** Current conversation input/messages */
139
148
  currentInput: LlmHistory;
140
149
  /** Current turn number (0-indexed, incremented at start of each turn) */
package/dist/esm/index.js CHANGED
@@ -482,6 +482,15 @@ function resolveModelChain(model) {
482
482
  * Providers can extend this class to reduce boilerplate.
483
483
  */
484
484
  class BaseProviderAdapter {
485
+ constructor() {
486
+ /**
487
+ * Whether OperateLoop may take a corrective turn when a `format` request
488
+ * completes with prose instead of structured output. Providers whose
489
+ * structured output rides a tool emulation (where compliance is a model
490
+ * decision) opt in; native grammar-constrained providers do not need it.
491
+ */
492
+ this.supportsStructuredOutputRetry = false;
493
+ }
485
494
  /**
486
495
  * Default implementation checks if error is retryable via classifyError
487
496
  */
@@ -3409,6 +3418,10 @@ class FireworksAdapter extends BaseProviderAdapter {
3409
3418
  super(...arguments);
3410
3419
  this.name = PROVIDER.FIREWORKS.NAME;
3411
3420
  this.defaultModel = PROVIDER.FIREWORKS.DEFAULT;
3421
+ // Structured output with tools rides the structured_output tool emulation
3422
+ // (Fireworks rejects response_format + tools), and emulation compliance is
3423
+ // a model decision — opt in to OperateLoop's corrective retry turn.
3424
+ this.supportsStructuredOutputRetry = true;
3412
3425
  // Session-level cache of models observed to reject native
3413
3426
  // `response_format: json_schema`. When a model is in this set, buildRequest
3414
3427
  // engages the legacy fake-tool path instead of native structured output.
@@ -3463,9 +3476,10 @@ class FireworksAdapter extends BaseProviderAdapter {
3463
3476
  const useFallbackStructuredOutput = Boolean(request.format) &&
3464
3477
  (hasCallerTools ||
3465
3478
  !this.supportsStructuredOutput(fireworksRequest.model));
3466
- const allTools = request.tools
3467
- ? [...request.tools]
3468
- : [];
3479
+ // On a corrective retry turn (the model answered a format request with
3480
+ // prose), offer only the structured_output tool so the demanded call is
3481
+ // the sole option.
3482
+ const allTools = request.tools && !request.structuredOutputRetry ? [...request.tools] : [];
3469
3483
  if (useFallbackStructuredOutput && request.format) {
3470
3484
  log$2.warn(hasCallerTools
3471
3485
  ? `[FireworksAdapter] Fireworks does not support response_format combined with tools; using structured_output tool emulation for model ${fireworksRequest.model}.`
@@ -8006,6 +8020,31 @@ function createErrorClassifier(adapter) {
8006
8020
  },
8007
8021
  };
8008
8022
  }
8023
+ /**
8024
+ * Attempt to read a prose response as the structured payload itself: parse the
8025
+ * text (stripping a Markdown code fence if present) and return the object, or
8026
+ * undefined when the text is not a JSON object.
8027
+ */
8028
+ function tryParseJsonObject(text) {
8029
+ let candidate = text.trim();
8030
+ const fence = candidate.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/);
8031
+ if (fence) {
8032
+ candidate = fence[1].trim();
8033
+ }
8034
+ if (!candidate.startsWith("{")) {
8035
+ return undefined;
8036
+ }
8037
+ try {
8038
+ const parsed = JSON.parse(candidate);
8039
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
8040
+ return parsed;
8041
+ }
8042
+ }
8043
+ catch {
8044
+ // Not JSON; caller falls through to the corrective-turn path.
8045
+ }
8046
+ return undefined;
8047
+ }
8009
8048
  //
8010
8049
  //
8011
8050
  // Main
@@ -8074,6 +8113,7 @@ class OperateLoop {
8074
8113
  messages: state.currentInput,
8075
8114
  model: modelName,
8076
8115
  providerOptions: options.providerOptions,
8116
+ structuredOutputRetry: state.structuredOutputRetry,
8077
8117
  system: options.system,
8078
8118
  temperature: options.temperature,
8079
8119
  tools: state.formattedTools,
@@ -8477,6 +8517,42 @@ class OperateLoop {
8477
8517
  return true; // Continue to next turn
8478
8518
  }
8479
8519
  }
8520
+ // Format contract enforcement: the loop is about to complete but the
8521
+ // model answered with prose instead of structured output.
8522
+ if (state.formattedFormat && typeof parsed.content === "string") {
8523
+ // First salvage attempt: the text may be the JSON itself (with or
8524
+ // without a code fence).
8525
+ const salvaged = tryParseJsonObject(parsed.content);
8526
+ if (salvaged) {
8527
+ state.responseBuilder.setContent(this.applyFormatArrayDefaults(salvaged, options));
8528
+ state.responseBuilder.complete();
8529
+ for (const item of this.adapter.responseToHistoryItems(parsed.raw)) {
8530
+ state.responseBuilder.appendToHistory(item);
8531
+ }
8532
+ return false; // Stop loop
8533
+ }
8534
+ // Corrective turn: for adapters whose structured output rides a tool
8535
+ // emulation, take another turn offering only the structured_output tool
8536
+ // and demand it be called. Bounded by maxTurns.
8537
+ if (this.adapter.supportsStructuredOutputRetry &&
8538
+ state.currentTurn < state.maxTurns) {
8539
+ log.warn(`[operate] Model returned text despite format on turn ${state.currentTurn}; retrying with structured_output tool only`);
8540
+ for (const item of this.adapter.responseToHistoryItems(parsed.raw)) {
8541
+ state.currentInput.push(item);
8542
+ state.responseBuilder.appendToHistory(item);
8543
+ }
8544
+ const corrective = {
8545
+ content: "You must provide your final answer by calling the structured_output tool " +
8546
+ "with arguments matching the required schema. Do not respond with text.",
8547
+ role: LlmMessageRole.User,
8548
+ type: LlmMessageType.Message,
8549
+ };
8550
+ state.currentInput.push(corrective);
8551
+ state.responseBuilder.appendToHistory(corrective);
8552
+ state.structuredOutputRetry = true;
8553
+ return true; // Continue to corrective turn
8554
+ }
8555
+ }
8480
8556
  // No tool calls or no toolkit - we're done
8481
8557
  state.responseBuilder.setContent(this.applyFormatArrayDefaults(parsed.content, options));
8482
8558
  state.responseBuilder.complete();