Repository navigation
fix(llm): map StructuredOutputMode across SDKs and update docs (#917) #976
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -115,57 +115,45 @@ class LlmApi { | |
| /// Generate a completion constrained to [schema]. | ||
| /// | ||
| /// [mode] picks how the schema is enforced: | ||
| /// - [StructuredOutputMode.validationOnly] (default): generate freely, then validate. | ||
| /// - [StructuredOutputMode.constrained] (default): engine-constrained decoding (GBNF grammar sampling). | ||
| /// - [StructuredOutputMode.validationOnly]: generate freely, then validate against the schema. | ||
| /// - [StructuredOutputMode.repair]: validate, then retry once with a repair instruction if invalid. | ||
| /// - [StructuredOutputMode.constrained]: engine-constrained decoding — fails | ||
| /// preflight until a constrained-decoding engine is wired in. | ||
| /// | ||
| /// Throws [SDKException] when [mode] cannot be honored, generation fails, | ||
| /// or the output cannot be parsed against the schema. | ||
| Future<StructuredResult> generateStructured( | ||
| String prompt, | ||
| String schema, { | ||
| StructuredOutputMode mode = StructuredOutputMode.validationOnly, | ||
| StructuredOutputMode mode = StructuredOutputMode.constrained, | ||
| LlmOptions? options, | ||
| }) async { | ||
| if (mode == StructuredOutputMode.constrained) { | ||
| throw SDKException.featureNotAvailable( | ||
| 'llm.generateStructured(mode: constrained) needs engine-level ' | ||
| 'constrained decoding, which is not wired in yet; use validationOnly or repair', | ||
| ); | ||
| } | ||
| await ModelGate.ensureLoaded( | ||
| modelId: options?.model, | ||
| category: ModelCategory.MODEL_CATEGORY_LANGUAGE, | ||
| ); | ||
| final effective = options ?? LlmOptions(); | ||
| final model = | ||
| await ModelGate.currentId(ModelCategory.MODEL_CATEGORY_LANGUAGE) ?? ''; | ||
| var proto = await RunAnywhereStructuredOutput.generateStructured( | ||
| final effectiveMode = options?.structuredOutput?.mode ?? mode; | ||
| final effective = (options ?? LlmOptions()).copyWith( | ||
| structuredOutput: StructuredOutput( | ||
| schema: schema, | ||
| mode: effectiveMode, | ||
| strict: options?.structuredOutput?.strict ?? true, | ||
| ), | ||
| toolChoice: ToolChoice.none, | ||
| ); | ||
| final protoOptions = effective.toProto(); | ||
| final generation = | ||
| await RunAnywhereStructuredOutput.generateWithStructuredOutput( | ||
| prompt: prompt, | ||
| structuredOutput: protoOptions.structuredOutput, | ||
| options: protoOptions, | ||
| ); | ||
| final proto = RunAnywhereLLM.shared.extractStructuredOutput( | ||
| text: generation.text, | ||
| schema: schema, | ||
| options: effective.toProto( | ||
| registeredTools: RunAnywhereTools.shared.getRegisteredTools(), | ||
| ), | ||
| ); | ||
|
Comment on lines
+150
to
153
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
rg -n "^import" bindings/flutter/packages/runanywhere/lib/public/api/namespaces/llm.dart
rg -nP "class\s+RunAnywhereLLM\b" bindings/flutterRepository: RunanywhereAI/runanywhere-sdks Length of output: 1495 🏁 Script executed: set -e
printf '%s\n' '--- llm.dart directives and cited call ---'
nl -ba bindings/flutter/packages/runanywhere/lib/public/api/namespaces/llm.dart | sed -n '1,35p;125,170p'
printf '%s\n' '--- structured-output extension ---'
nl -ba bindings/flutter/packages/runanywhere/lib/public/extensions/runanywhere_structured_output.dart | sed -n '1,220p'
printf '%s\n' '--- RunAnywhereLLM declaration and relevant members ---'
nl -ba bindings/flutter/packages/runanywhere/lib/public/capabilities/runanywhere_llm.dart | sed -n '1,80p;300,390p'Repository: RunanywhereAI/runanywhere-sdks Length of output: 16118 Import
🐛 Suggested fix import 'package:runanywhere/public/capabilities/runanywhere_tools.dart';
+import 'package:runanywhere/public/capabilities/runanywhere_llm.dart';
import 'package:runanywhere/public/extensions/runanywhere_structured_output.dart';🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, looking into this. |
||
| if (mode == StructuredOutputMode.repair && !proto.validation.isValid) { | ||
| final repairPrompt = | ||
| '$prompt\n\n' | ||
| 'Your previous answer did not match the required JSON schema. ' | ||
| 'Reply again with ONLY JSON that satisfies this schema.\n\n' | ||
| 'Previous invalid answer: ' | ||
| '${proto.hasRawText() ? proto.rawText : ''}'; | ||
| proto = await RunAnywhereStructuredOutput.generateStructured( | ||
| prompt: repairPrompt, | ||
| schema: schema, | ||
| options: effective.toProto( | ||
| registeredTools: RunAnywhereTools.shared.getRegisteredTools(), | ||
| ), | ||
| ); | ||
| } | ||
| return StructuredResult.fromProto( | ||
| proto, | ||
| GenerationResult(text: proto.hasRawText() ? proto.rawText : '', model: model), | ||
| GenerationResult.fromLlm(generation), | ||
| ); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,8 @@ import 'package:runanywhere/generated/rag.pb.dart' show RAGConfiguration; | |
| import 'package:runanywhere/generated/segmentation.pb.dart' as seg_pb; | ||
| import 'package:runanywhere/generated/structured_output.pb.dart' | ||
| show StructuredOutputOptions; | ||
| import 'package:runanywhere/generated/structured_output.pbenum.dart' | ||
| as so_pb; | ||
| import 'package:runanywhere/generated/stt_options.pb.dart' show STTOptions; | ||
| import 'package:runanywhere/generated/thinking_tag_pattern.pb.dart' | ||
| as think_pb; | ||
|
|
@@ -145,7 +147,11 @@ class ToolChoice { | |
| /// Schema constraint applied to a generation's output. | ||
| class StructuredOutput { | ||
| /// Constrain output to [schema]. | ||
| const StructuredOutput({required this.schema, this.strict = true}); | ||
| const StructuredOutput({ | ||
| required this.schema, | ||
| this.mode = StructuredOutputMode.constrained, | ||
| this.strict = true, | ||
| }); | ||
|
|
||
| /// JSON Schema the output must satisfy, as raw JSON Schema text. | ||
| /// | ||
|
|
@@ -157,6 +163,9 @@ class StructuredOutput { | |
| /// callers must already hold the schema as text. | ||
| final String schema; | ||
|
|
||
| /// How the schema constraint is enforced during generation. | ||
| final StructuredOutputMode mode; | ||
|
|
||
| /// True rejects output that does not validate instead of repairing it. | ||
| /// | ||
| /// `StructuredOutputOptions.strict_mode` was deleted outright — this knob | ||
|
|
@@ -165,8 +174,24 @@ class StructuredOutput { | |
| final bool strict; | ||
|
|
||
| /// Build the generated structured-output options. | ||
| StructuredOutputOptions toProto() => | ||
| StructuredOutputOptions(schema: schema, includeSchemaInPrompt: true); | ||
| StructuredOutputOptions toProto() { | ||
| final proto = StructuredOutputOptions( | ||
| schema: schema, | ||
| includeSchemaInPrompt: true, | ||
| ); | ||
| switch (mode) { | ||
| case StructuredOutputMode.constrained: | ||
| proto.mode = so_pb.StructuredOutputMode.STRUCTURED_OUTPUT_MODE_CONSTRAINED; | ||
| break; | ||
| case StructuredOutputMode.validationOnly: | ||
| proto.mode = so_pb.StructuredOutputMode.STRUCTURED_OUTPUT_MODE_VALIDATION_ONLY; | ||
| break; | ||
| case StructuredOutputMode.repair: | ||
| proto.mode = so_pb.StructuredOutputMode.STRUCTURED_OUTPUT_MODE_REPAIR; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Mapping Prompt for AI agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, looking into this. |
||
| break; | ||
| } | ||
| return proto; | ||
| } | ||
| } | ||
|
|
||
| /// Sampling, prompting, reasoning, and tool controls for `llm` and `vlm`. | ||
|
|
@@ -304,6 +329,47 @@ class LlmOptions { | |
| return proto; | ||
| } | ||
|
|
||
| /// Create a copy with modified fields. | ||
| LlmOptions copyWith({ | ||
| String? model, | ||
| int? maxOutputTokens, | ||
| double? temperature, | ||
| double? topP, | ||
| int? topK, | ||
| double? minP, | ||
| double? frequencyPenalty, | ||
| double? presencePenalty, | ||
| double? repetitionPenalty, | ||
| int? seed, | ||
| List<String>? stopSequences, | ||
| String? systemPrompt, | ||
| ReasoningOptions? reasoning, | ||
| StructuredOutput? structuredOutput, | ||
| List<ToolDefinition>? tools, | ||
| ToolChoice? toolChoice, | ||
| int? maxToolCalls, | ||
| bool? autoExecute, | ||
| }) => LlmOptions( | ||
| model: model ?? this.model, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Prompt for AI agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, looking into this. |
||
| maxOutputTokens: maxOutputTokens ?? _argMaxOutputTokens, | ||
| temperature: temperature ?? _argTemperature, | ||
| topP: topP ?? _argTopP, | ||
| topK: topK ?? this.topK, | ||
| minP: minP ?? this.minP, | ||
| frequencyPenalty: frequencyPenalty ?? this.frequencyPenalty, | ||
| presencePenalty: presencePenalty ?? this.presencePenalty, | ||
| repetitionPenalty: repetitionPenalty ?? _argRepetitionPenalty, | ||
| seed: seed ?? this.seed, | ||
| stopSequences: stopSequences ?? this.stopSequences, | ||
| systemPrompt: systemPrompt ?? this.systemPrompt, | ||
| reasoning: reasoning ?? this.reasoning, | ||
| structuredOutput: structuredOutput ?? this.structuredOutput, | ||
| tools: tools ?? this.tools, | ||
| toolChoice: toolChoice ?? this.toolChoice, | ||
| maxToolCalls: maxToolCalls ?? _argMaxToolCalls, | ||
| autoExecute: autoExecute ?? this.autoExecute, | ||
| ); | ||
|
|
||
| /// Build the generated VLM request envelope for [prompt] over [images]. | ||
| /// | ||
| /// `VLMGenerationOptions` was deleted outright (idl/vlm_options.proto): | ||
|
|
@@ -840,7 +906,7 @@ class LoadOptions { | |
|
|
||
| /// Enforcement level `llm.generateStructured` applies to its schema. | ||
| enum StructuredOutputMode { | ||
| /// Engine-constrained decoding; fails preflight until wired in. | ||
| /// Engine-constrained decoding (GBNF grammar sampling). | ||
| constrained, | ||
|
|
||
| /// Generate freely, then validate against the schema. | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -126,42 +126,32 @@ public class LlmNamespace internal constructor() { | |||||
| * Generate a completion constrained to [schema] and parse it. | ||||||
| * | ||||||
| * [mode] picks how the schema is enforced: | ||||||
| * - [StructuredOutputMode.VALIDATION_ONLY] (default): generate freely, then validate. | ||||||
| * - [StructuredOutputMode.CONSTRAINED] (default): engine-constrained decoding (GBNF grammar sampling). | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The new default is not universally available: grammar-constrained decoding depends on the loaded backend. Qualify this public API claim with “where supported” and state that unsupported backends fail or require Prompt for AI agents
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, looking into this. |
||||||
| * - [StructuredOutputMode.VALIDATION_ONLY]: generate freely, then validate against the schema. | ||||||
| * - [StructuredOutputMode.REPAIR]: validate, then retry once with a repair instruction if invalid. | ||||||
| * - [StructuredOutputMode.CONSTRAINED]: engine-constrained decoding — fails preflight | ||||||
| * until a constrained-decoding engine is wired in. | ||||||
| * | ||||||
| * @throws SDKException when no language model can be loaded, [mode] cannot | ||||||
| * be honored, or generation fails. | ||||||
| */ | ||||||
| public suspend fun generateStructured( | ||||||
| prompt: String, | ||||||
| schema: JsonSchema, | ||||||
| mode: StructuredOutputMode = StructuredOutputMode.VALIDATION_ONLY, | ||||||
| mode: StructuredOutputMode = StructuredOutputMode.CONSTRAINED, | ||||||
| options: LlmOptions? = null, | ||||||
| ): StructuredResult { | ||||||
| if (mode == StructuredOutputMode.CONSTRAINED) { | ||||||
| throw SDKException.unsupportedCapability( | ||||||
| "llm.generateStructured(mode = CONSTRAINED)", | ||||||
| "needs engine-level constrained decoding, which is not wired in yet; use VALIDATION_ONLY or REPAIR", | ||||||
| ) | ||||||
| } | ||||||
| val opts = options.orDefault() | ||||||
| val structuredOutput = StructuredOutput(schema = schema) | ||||||
| var generation = generateStructuredUnary(prompt, opts, structuredOutput) | ||||||
| var parsed = parseStructuredOutput(generation, structuredOutput) | ||||||
|
|
||||||
| if (mode == StructuredOutputMode.REPAIR && parsed.validation?.is_valid != true) { | ||||||
| val repairPrompt = structuredRepairPrompt(prompt, generation.text, schema) | ||||||
| generation = generateStructuredUnary(repairPrompt, opts, structuredOutput) | ||||||
| parsed = parseStructuredOutput(generation, structuredOutput) | ||||||
| } | ||||||
| val opts = options.orDefault().copy(toolChoice = ToolChoice.None) | ||||||
| val effectiveSchema = opts.structuredOutput?.schema ?: schema | ||||||
| val effectiveMode = opts.structuredOutput?.mode ?: mode | ||||||
| val effectiveStrict = opts.structuredOutput?.strict ?: true | ||||||
| val structuredOutput = StructuredOutput(schema = effectiveSchema, strict = effectiveStrict, mode = effectiveMode) | ||||||
| val generation = generateStructuredUnary(prompt, opts, structuredOutput) | ||||||
| val parsed = parseStructuredOutput(generation, structuredOutput) | ||||||
|
|
||||||
| return StructuredResult( | ||||||
| value = parsed.json, | ||||||
| raw = parsed.raw_text ?: generation.text, | ||||||
| valid = parsed.validation?.is_valid == true, | ||||||
| mode = mode, | ||||||
| mode = effectiveMode, | ||||||
| inputTokens = generation.inputTokens, | ||||||
| outputTokens = generation.outputTokens, | ||||||
| timeToFirstTokenMs = generation.timeToFirstTokenMs, | ||||||
|
|
@@ -262,17 +252,6 @@ public class LlmNamespace internal constructor() { | |||||
|
|
||||||
| private fun newRequestId(): String = UUID.randomUUID().toString() | ||||||
|
|
||||||
| /** The one retry prompt `generateStructured(mode = REPAIR)` sends when the first pass did not validate. */ | ||||||
| private fun structuredRepairPrompt(original: String, invalidOutput: String, schema: JsonSchema): String = | ||||||
| """ | ||||||
| $original | ||||||
|
|
||||||
| Your previous answer did not match the required JSON schema. Reply again with ONLY JSON that satisfies this schema. | ||||||
|
|
||||||
| Schema: ${schema.rawJson} | ||||||
| Previous invalid answer: $invalidOutput | ||||||
| """.trimIndent() | ||||||
|
|
||||||
| private fun LlmOptions.usesTools(): Boolean = toolChoice != ToolChoice.None | ||||||
|
|
||||||
| /** | ||||||
|
|
||||||
Uh oh!
There was an error while loading. Please reload this page.