structout

package module
v0.0.0-...-5f103ee Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 19, 2026 License: MIT Imports: 9 Imported by: 0

README

structout

Slightly better structured output on local LLMs used with Genkit

structout is a Go library for the Firebase Genkit ecosystem. It coerces small/local LLMs (Ollama with gemma, qwen, etc.) into producing JSON that conforms to a Go type T.

Genkit's default structured-output path appends the JSON schema as text to the prompt and hopes the model honors it. Larger hosted models do; smaller local models frequently don't. structout sidesteps the problem by exposing the schema as a tool (which local models respect far better) and using a model middleware to rewrite the tool call into the final JSON text — without an extra round-trip to the model.

Install

go get github.com/falmar/structout

Usage

package main

import (
    "context"
    "fmt"

    "github.com/falmar/structout"
    "github.com/firebase/genkit/go/ai"
    "github.com/firebase/genkit/go/genkit"
    _ "github.com/firebase/genkit/go/plugins/ollama"
)

type Jedi struct {
    Name       string `json:"name"`
    Lightsaber string `json:"lightsaber" jsonschema:"enum=blue,enum=green,enum=purple"`
}

func main() {
    ctx := context.Background()
    g, err := genkit.Init(ctx /* your plugins / options */)
    if err != nil {
        panic(err)
    }

    so := structout.Define[Jedi](g)

    jedi, _, err := genkit.GenerateData[Jedi](ctx, g,
        ai.WithModelName("ollama/gemma4:e4b"),
        ai.WithSystem("You are a Jedi master."+so.Instruction),
        ai.WithPrompt("Introduce yourself."),
        ai.WithTools(so.Tool),
        ai.WithMiddleware(so.Middleware),
    )
    if err != nil {
        panic(err)
    }

    fmt.Printf("%s wields a %s lightsaber\n", jedi.Name, jedi.Lightsaber)
}

Define[T] returns three pieces you wire into your Generate/GenerateData call:

  • so.Tool — the formatter tool. Its input schema is generated from T. The tool body never runs.
  • so.Middleware — rewrites the model's tool call into plain JSON text so GenerateData[T] can unmarshal it.
  • so.Instruction — system-prompt fragment telling the model to call the tool and emit its input verbatim. Append to your own system message.

How it works

  1. The model sees a tool whose input schema is T.
  2. It calls the tool with T-shaped arguments.
  3. The middleware intercepts the response before Genkit dispatches the tool, marshals the call's input to JSON, and replaces resp.Message with that text.
  4. GenerateData[T] validates and unmarshals the text into *T.
  5. Native Integration: By utilizing Genkit's built-in middleware and tool registration, structout maintains the integrity of Genkit's traces and plugins.

No second model call, no interrupt error in traces, and full tool-input validation happens via Genkit's output parser on the way out.

Caveats

  • The formatter must be the only tool call in its turn. Other turns (before or after) can call any tools you like. If the model emits the formatter alongside another tool in the same response, the middleware passes through and the model typically loops. Prompt it to finish other tool use first and call the formatter alone.
  • No streaming. When a stream callback is active the middleware does nothing; the caller has to handle raw tool-request chunks.
  • Don't combine with ai.WithOutputSchema / custom output formats. GenerateData[T] already schema-validates the synthesized text; an additional output handler fights with the middleware and produces confusing errors.
  • Process-global tool memoization. Define[T] caches tools by reflect.Type. Multiple *genkit.Genkit instances in the same process with the same T share the tool registered on whichever instance called Define[T] first.

Requirements

Documentation

Overview

Package structout provides composable util functions for genkit enforcing structured output by tool-calling

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Output

type Output[T any] struct {
	// Tool is the formatter tool exposed to the model. Its input schema is
	// generated from T. The tool body never runs — [Output.Middleware]
	// rewrites the response before genkit's tool loop can dispatch it.
	Tool *ai.ToolDef[T, any]

	// Middleware intercepts the model response after generation. When the
	// model returns the formatter tool as the only tool request in the turn,
	// the middleware marshals its input to JSON text and replaces
	// resp.Message so the tool loop exits cleanly. Pass to the Generate call
	// via ai.WithMiddleware.
	//
	// Caveats:
	//   - Only the single-formatter case is rewritten. If the model emits
	//     the formatter alongside other tool calls in the same turn, the
	//     middleware passes through; the other tools run and the model
	//     typically loops. Prompt the model to complete other tool use
	//     first, then call the formatter on its own turn.
	//   - Streaming is not supported. When a stream callback is present,
	//     the middleware passes through unchanged and the caller must
	//     handle the raw tool-request chunks themselves.
	//   - Do not combine with ai.WithOutputSchema or a non-default output
	//     format on the same request. [genkit.GenerateData] already
	//     validates and unmarshals the synthesized text against T.
	Middleware ai.ModelMiddleware

	// Instruction is a system-prompt fragment that tells the model to call
	// Tool and emit its output JSON verbatim. Append it to a system message
	// of your choice; it references the tool by its registered name.
	Instruction string
}

Output bundles the tool, middleware, and system-prompt instruction needed to coerce an LLM into producing a value of type T via tool-calling. It is the return value of Define.

The tool's input schema is derived from T; the middleware intercepts the model's tool call and rewrites the response to plain JSON text, so genkit.GenerateData can validate and unmarshal it into T.

Example:

type Jedi struct {
    Name       string `json:"name"`
    Lightsaber string `json:"lightsaber" jsonschema:"enum=blue,enum=green,enum=purple"`
}

so := structout.Define[Jedi](g)
jedi, resp, err := genkit.GenerateData[Jedi](ctx, g,
    ai.WithModelName("ollama/gemma4:e4b"),
    ai.WithSystem("You are a Jedi master." + so.Instruction),
    ai.WithPrompt("Introduce yourself."),
    ai.WithTools(so.Tool),
    ai.WithMiddleware(so.Middleware),
)

func Define

func Define[T any](g *genkit.Genkit) *Output[T]

Define registers a formatter tool for type T on g and returns an Output bundling the tool, a response-rewriting middleware, and a system-prompt instruction.

T should be a JSON-serializable struct; scalars and maps work but give the model a weaker schema to target.

Tools are memoized by reflect.Type: a second call to Define[T] with the same T returns the first registration. The memoization is process-global and is NOT scoped to a *genkit.Genkit instance — using multiple genkit instances in the same process with the same T will share the tool registered on whichever instance called Define[T] first.

Directories

Path Synopsis
examples
basic command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL