Skip to content

Commit 1266c03

Browse files
bchapuisclaude
andcommitted
Add schema input to LLM nodes for structured JSON output
Enable structured output via JSON schema across Workers AI, OpenAI, and Gemini LLM nodes. When a schema is selected, the executor converts it to JSON Schema and passes it to each provider's native structured output API. Output type changed from string to any to support JSON responses. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dfc591c commit 1266c03

22 files changed

Lines changed: 233 additions & 42 deletions

packages/runtime/src/nodes/gemini/execute-gemini-model.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type { NodeContext, ToolReference } from "@dafthunk/runtime";
2-
import type { NodeExecution } from "@dafthunk/types";
2+
import type { NodeExecution, Schema } from "@dafthunk/types";
33
import { GoogleGenAI } from "@google/genai";
44
import { getGoogleAIConfig } from "../../utils/ai-gateway";
5+
import { schemaToJsonSchema } from "../../utils/schema-to-json-schema";
56
import { calculateTokenUsage, type TokenPricing } from "../../utils/usage";
67

78
interface GeminiModelConfig {
@@ -41,6 +42,7 @@ export async function executeGeminiModel(
4142
thinking_budget,
4243
tools,
4344
googleSearch,
45+
schema: schemaInput,
4446
} = context.inputs;
4547

4648
if (!input) {
@@ -64,6 +66,16 @@ export async function executeGeminiModel(
6466
genConfig.thinkingConfig = { thinkingBudget: thinking_budget };
6567
}
6668

69+
// Add structured output when a schema is provided
70+
if (
71+
schemaInput &&
72+
typeof schemaInput === "object" &&
73+
"fields" in schemaInput
74+
) {
75+
genConfig.responseMimeType = "application/json";
76+
genConfig.responseSchema = schemaToJsonSchema(schemaInput as Schema);
77+
}
78+
6779
const functionDeclarations =
6880
await node.convertFunctionCallsToGeminiDeclarations(
6981
tools as ToolReference[],

packages/runtime/src/nodes/gemini/gemini-2-5-flash-node.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,18 @@ export class Gemini25FlashNode extends ExecutableNode {
7575
required: false,
7676
hidden: true,
7777
},
78+
{
79+
name: "schema",
80+
type: "schema",
81+
description: "JSON schema to constrain the output format",
82+
hidden: true,
83+
},
7884
],
7985
outputs: [
8086
{
8187
name: "text",
82-
type: "string",
83-
description: "Generated text response from Gemini 2.5 Flash",
88+
type: "any",
89+
description: "Generated text or JSON response from Gemini 2.5 Flash",
8490
},
8591
{
8692
name: "candidates",

packages/runtime/src/nodes/gemini/gemini-2-5-pro-node.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,18 @@ export class Gemini25ProNode extends ExecutableNode {
7575
required: false,
7676
hidden: true,
7777
},
78+
{
79+
name: "schema",
80+
type: "schema",
81+
description: "JSON schema to constrain the output format",
82+
hidden: true,
83+
},
7884
],
7985
outputs: [
8086
{
8187
name: "text",
82-
type: "string",
83-
description: "Generated text response from Gemini 2.5 Pro",
88+
type: "any",
89+
description: "Generated text or JSON response from Gemini 2.5 Pro",
8490
},
8591
{
8692
name: "candidates",

packages/runtime/src/nodes/gemini/gemini-3-1-pro-node.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,18 @@ export class Gemini31ProNode extends ExecutableNode {
7575
required: false,
7676
hidden: true,
7777
},
78+
{
79+
name: "schema",
80+
type: "schema",
81+
description: "JSON schema to constrain the output format",
82+
hidden: true,
83+
},
7884
],
7985
outputs: [
8086
{
8187
name: "text",
82-
type: "string",
83-
description: "Generated text response from Gemini 3.1 Pro",
88+
type: "any",
89+
description: "Generated text or JSON response from Gemini 3.1 Pro",
8490
},
8591
{
8692
name: "candidates",

packages/runtime/src/nodes/gemini/gemini-3-flash-node.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,18 @@ export class Gemini3FlashNode extends ExecutableNode {
7575
required: false,
7676
hidden: true,
7777
},
78+
{
79+
name: "schema",
80+
type: "schema",
81+
description: "JSON schema to constrain the output format",
82+
hidden: true,
83+
},
7884
],
7985
outputs: [
8086
{
8187
name: "text",
82-
type: "string",
83-
description: "Generated text response from Gemini 3 Flash",
88+
type: "any",
89+
description: "Generated text or JSON response from Gemini 3 Flash",
8490
},
8591
{
8692
name: "candidates",

packages/runtime/src/nodes/openai/execute-openai-model.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type { NodeContext } from "@dafthunk/runtime";
2-
import type { NodeExecution } from "@dafthunk/types";
2+
import type { NodeExecution, Schema } from "@dafthunk/types";
33
import OpenAI from "openai";
44
import { getOpenAIConfig } from "../../utils/ai-gateway";
5+
import { schemaToJsonSchema } from "../../utils/schema-to-json-schema";
56
import { calculateTokenUsage, type TokenPricing } from "../../utils/usage";
67

78
/**
@@ -23,7 +24,7 @@ export async function executeOpenAIModel(
2324
pricing: TokenPricing
2425
): Promise<NodeExecution> {
2526
try {
26-
const { instructions, input } = context.inputs;
27+
const { instructions, input, schema: schemaInput } = context.inputs;
2728

2829
if (!input) {
2930
return node.createErrorResult("Input is required");
@@ -35,6 +36,21 @@ export async function executeOpenAIModel(
3536
...getOpenAIConfig(context.env),
3637
});
3738

39+
// Build response_format when a schema is provided
40+
const responseFormat =
41+
schemaInput &&
42+
typeof schemaInput === "object" &&
43+
"fields" in schemaInput
44+
? {
45+
type: "json_schema" as const,
46+
json_schema: {
47+
name: "response",
48+
schema: schemaToJsonSchema(schemaInput as Schema),
49+
strict: true,
50+
},
51+
}
52+
: undefined;
53+
3854
const completion = await client.chat.completions.create({
3955
model: modelId,
4056
max_tokens: 1024,
@@ -44,6 +60,7 @@ export async function executeOpenAIModel(
4460
: []),
4561
{ role: "user" as const, content: input },
4662
],
63+
...(responseFormat && { response_format: responseFormat }),
4764
});
4865

4966
const responseText = completion.choices[0]?.message?.content || "";

packages/runtime/src/nodes/openai/gpt-41-node.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,18 @@ export class Gpt41Node extends ExecutableNode {
3434
description: "The input text or question for GPT-4.1",
3535
required: true,
3636
},
37+
{
38+
name: "schema",
39+
type: "schema",
40+
description: "JSON schema to constrain the output format",
41+
hidden: true,
42+
},
3743
],
3844
outputs: [
3945
{
4046
name: "text",
41-
type: "string",
42-
description: "Generated text response from GPT-4.1",
47+
type: "any",
48+
description: "Generated text or JSON response from GPT-4.1",
4349
},
4450
],
4551
};

packages/runtime/src/nodes/openai/gpt-5-mini-node.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,18 @@ export class Gpt5MiniNode extends ExecutableNode {
3434
description: "The input text or question for GPT-5 Mini",
3535
required: true,
3636
},
37+
{
38+
name: "schema",
39+
type: "schema",
40+
description: "JSON schema to constrain the output format",
41+
hidden: true,
42+
},
3743
],
3844
outputs: [
3945
{
4046
name: "text",
41-
type: "string",
42-
description: "Generated text response from GPT-5 Mini",
47+
type: "any",
48+
description: "Generated text or JSON response from GPT-5 Mini",
4349
},
4450
],
4551
};

packages/runtime/src/nodes/openai/gpt-5-nano-node.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,18 @@ export class Gpt5NanoNode extends ExecutableNode {
3434
description: "The input text or question for GPT-5 Nano",
3535
required: true,
3636
},
37+
{
38+
name: "schema",
39+
type: "schema",
40+
description: "JSON schema to constrain the output format",
41+
hidden: true,
42+
},
3743
],
3844
outputs: [
3945
{
4046
name: "text",
41-
type: "string",
42-
description: "Generated text response from GPT-5 Nano",
47+
type: "any",
48+
description: "Generated text or JSON response from GPT-5 Nano",
4349
},
4450
],
4551
};

packages/runtime/src/nodes/openai/gpt-5-node.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,18 @@ export class Gpt5Node extends ExecutableNode {
3434
description: "The input text or question for GPT-5",
3535
required: true,
3636
},
37+
{
38+
name: "schema",
39+
type: "schema",
40+
description: "JSON schema to constrain the output format",
41+
hidden: true,
42+
},
3743
],
3844
outputs: [
3945
{
4046
name: "text",
41-
type: "string",
42-
description: "Generated text response from GPT-5",
47+
type: "any",
48+
description: "Generated text or JSON response from GPT-5",
4349
},
4450
],
4551
};

0 commit comments

Comments
 (0)