Skip to content

Commit f4de1b0

Browse files
authored
Improve unstable AI structured output handling for empty tool params and add Tool.EmptyParams, closes #1749 (#1750)
1 parent 6de4efe commit f4de1b0

File tree

6 files changed

+57
-0
lines changed

6 files changed

+57
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"effect": patch
3+
---
4+
5+
Improve unstable AI structured output handling for empty tool params and add `Tool.EmptyParams`, closes #1749.

packages/effect/src/unstable/ai/AnthropicStructuredOutput.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import * as Predicate from "../../Predicate.ts"
2222
import * as Schema from "../../Schema.ts"
2323
import * as AST from "../../SchemaAST.ts"
2424
import * as Transformation from "../../SchemaTransformation.ts"
25+
import * as Tool from "./Tool.ts"
2526

2627
/**
2728
* Transforms a `Schema.Codec` into a form compatible with Anthropic's
@@ -225,6 +226,9 @@ function recur(ast: AST.AST): AST.AST {
225226
}
226227
} else if (ast.indexSignatures.length === 1 && ast.propertySignatures.length === 0) {
227228
const is = ast.indexSignatures[0]
229+
if (Tool.isEmptyParamsRecord(is)) {
230+
return ast
231+
}
228232
// records are not supported by Anthropic, so we translate them to arrays of key-value pairs
229233
if (annotations !== undefined && typeof annotations.description === "string") {
230234
annotations.description = `${RECORD_DESCRIPTION}; ${annotations.description}`

packages/effect/src/unstable/ai/OpenAiStructuredOutput.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as Rec from "../../Record.ts"
1111
import * as Schema from "../../Schema.ts"
1212
import * as AST from "../../SchemaAST.ts"
1313
import * as Transformation from "../../SchemaTransformation.ts"
14+
import * as Tool from "./Tool.ts"
1415

1516
/**
1617
* Transforms a `Schema.Codec` into a form compatible with OpenAI's
@@ -243,6 +244,9 @@ function recurOpenAI(ast: AST.AST): AST.AST {
243244
}
244245
} else if (ast.indexSignatures.length === 1 && ast.propertySignatures.length === 0) {
245246
const is = ast.indexSignatures[0]
247+
if (Tool.isEmptyParamsRecord(is)) {
248+
return ast
249+
}
246250
// records are not supported by OpenAI, so we translate them to arrays of key-value pairs
247251
if (annotations !== undefined && typeof annotations.description === "string") {
248252
annotations.description = `${RECORD_DESCRIPTION}; ${annotations.description}`

packages/effect/src/unstable/ai/Tool.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,6 +1129,9 @@ const dynamicProto = <
11291129
* can call. The tool definition includes parameter validation, success/failure
11301130
* schemas, and optional service dependencies.
11311131
*
1132+
* If a tool accepts no parameters but still needs an explicit empty object
1133+
* schema, use {@link EmptyParams}.
1134+
*
11321135
* @example
11331136
* ```ts
11341137
* import { Schema } from "effect"
@@ -1830,3 +1833,20 @@ export const unsafeSecureJsonParse = (text: string): unknown => {
18301833
Error.stackTraceLimit = stackTraceLimit
18311834
}
18321835
}
1836+
1837+
/**
1838+
* @since 4.0.0
1839+
*/
1840+
export interface EmptyParams extends Schema.$Record<Schema.String, Schema.Never> {}
1841+
1842+
/**
1843+
* A schema for tools that accept no parameters.
1844+
*
1845+
* @since 4.0.0
1846+
*/
1847+
export const EmptyParams: EmptyParams = Schema.Record(Schema.String, Schema.Never)
1848+
1849+
/** @internal */
1850+
export function isEmptyParamsRecord(indexSignature: AST.IndexSignature): boolean {
1851+
return indexSignature.parameter === AST.string && AST.isNever(indexSignature.type)
1852+
}

packages/effect/test/unstable/ai/AnthropicStructuredOutput.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { assert, describe, it } from "@effect/vitest"
22
import { type JsonSchema, Schema } from "effect"
33
import { TestSchema } from "effect/testing"
44
import { toCodecAnthropic } from "effect/unstable/ai/AnthropicStructuredOutput"
5+
import * as Tool from "effect/unstable/ai/Tool"
56

67
function assertJsonSchema(schema: Schema.Top, expected: JsonSchema.JsonSchema) {
78
assert.deepStrictEqual(toCodecAnthropic(schema).jsonSchema, expected)
@@ -464,6 +465,17 @@ describe("toCodecAnthropic", () => {
464465
})
465466

466467
describe("Record", () => {
468+
it("EmptyParams", async () => {
469+
assertJsonSchema(Tool.EmptyParams, {
470+
"type": "object",
471+
"additionalProperties": false
472+
})
473+
assertJsonSchema(Schema.Record(Schema.String, Schema.Never), {
474+
"type": "object",
475+
"additionalProperties": false
476+
})
477+
})
478+
467479
it("Record(String, Finite)", async () => {
468480
const schema = Schema.Record(Schema.String, Schema.Finite)
469481
assertJsonSchema(schema, {

packages/effect/test/unstable/ai/OpenAiStructuredOutput.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { assert, describe, it } from "@effect/vitest"
22
import { type JsonSchema, Schema } from "effect"
33
import { TestSchema } from "effect/testing"
44
import { toCodecOpenAI } from "effect/unstable/ai/OpenAiStructuredOutput"
5+
import * as Tool from "effect/unstable/ai/Tool"
56

67
function assertJsonSchema(schema: Schema.Top, expected: JsonSchema.JsonSchema) {
78
assert.deepStrictEqual(toCodecOpenAI(schema).jsonSchema, expected)
@@ -592,6 +593,17 @@ describe("toCodecOpenAI", () => {
592593
})
593594

594595
describe("Record", () => {
596+
it("EmptyParams", async () => {
597+
assertJsonSchema(Tool.EmptyParams, {
598+
"type": "object",
599+
"additionalProperties": false
600+
})
601+
assertJsonSchema(Schema.Record(Schema.String, Schema.Never), {
602+
"type": "object",
603+
"additionalProperties": false
604+
})
605+
})
606+
595607
it("Record(String, Finite)", async () => {
596608
const schema = Schema.Record(Schema.String, Schema.Finite)
597609
assertJsonSchema(schema, {

0 commit comments

Comments
 (0)