| 1 | import { assert, describe, it } from "@effect/vitest" |
| 2 | import { Schema } from "effect" |
| 3 | import { toCodecAnthropic } from "effect/ai/AnthropicStructuredOutput" |
| 4 | import { TestSchema } from "effect/testing" |
| 5 | |
| 6 | describe("AnthropicStructuredOutput representation v2", () => { |
| 7 | it("projects the encoded side before JSON Schema generation", () => { |
| 8 | assert.strictEqual(toCodecAnthropic(Schema.FiniteFromString).jsonSchema.type, "string") |
| 9 | }) |
| 10 | |
| 11 | it("keeps supported custom JSON Schema filters with a description", () => { |
| 12 | const schema = Schema.String.check(Schema.makeFilter<string>((value) => value.startsWith("a"), { |
| 13 | description: "starts with a", |
| 14 | representation: { |
| 15 | id: "test/ai/anthropic/startsWithA", |
| 16 | payload: null |
| 17 | }, |
| 18 | toJsonSchema: () => ({ pattern: "^a" }) |
| 19 | })) |
| 20 | |
| 21 | assert.deepStrictEqual(toCodecAnthropic(schema).jsonSchema, { |
| 22 | type: "string", |
| 23 | description: "starts with a", |
| 24 | pattern: "^a" |
| 25 | }) |
| 26 | }) |
| 27 | |
| 28 | it("drops unsupported custom JSON Schema filters with a description", async () => { |
| 29 | const schema = Schema.String.check(Schema.makeFilter<string>((value) => value.length >= 2, { |
| 30 | description: "at least two characters", |
| 31 | representation: { |
| 32 | id: "test/ai/anthropic/minTwoCharactersWithDescription", |
| 33 | payload: null |
| 34 | }, |
| 35 | toJsonSchema: () => ({ minLength: 2 }) |
| 36 | })) |
| 37 | |
| 38 | const result = toCodecAnthropic(schema) |
| 39 | assert.deepStrictEqual(result.jsonSchema, { |
| 40 | type: "string", |
| 41 | description: "at least two characters" |
| 42 | }) |
| 43 | await new TestSchema.Asserts(result.codec).decoding().fail( |
| 44 | "a", |
| 45 | `Expected <filter>` |
| 46 | ) |
| 47 | }) |
| 48 | |
| 49 | it("keeps supported custom JSON Schema filters without a description", () => { |
| 50 | const schema = Schema.String.check(Schema.makeFilter<string>((value) => value.startsWith("a"), { |
| 51 | representation: { |
| 52 | id: "test/ai/anthropic/startsWithAWithoutDescription", |
| 53 | payload: null |
| 54 | }, |
| 55 | toJsonSchema: () => ({ pattern: "^a" }) |
| 56 | })) |
| 57 | |
| 58 | assert.deepStrictEqual(toCodecAnthropic(schema).jsonSchema, { |
| 59 | type: "string", |
| 60 | pattern: "^a" |
| 61 | }) |
| 62 | }) |
| 63 | |
| 64 | it("drops unsupported custom JSON Schema filters without a description", () => { |
| 65 | const schema = Schema.String.check(Schema.makeFilter<string>((value) => value.length >= 2, { |
| 66 | representation: { |
| 67 | id: "test/ai/anthropic/minTwoCharactersWithoutDescription", |
| 68 | payload: null |
| 69 | }, |
| 70 | toJsonSchema: () => ({ minLength: 2 }) |
| 71 | })) |
| 72 | |
| 73 | assert.deepStrictEqual(toCodecAnthropic(schema).jsonSchema, { |
| 74 | type: "string" |
| 75 | }) |
| 76 | }) |
| 77 | |
| 78 | it("invokes custom JSON Schema callbacks only with compiler inputs", () => { |
| 79 | let invocations = 0 |
| 80 | const schema = Schema.String.check(Schema.makeFilter<string>(() => true, { |
| 81 | representation: { |
| 82 | id: "test/ai/anthropic/compilerInputs", |
| 83 | payload: null, |
| 84 | schemas: [Schema.Finite.ast] |
| 85 | }, |
| 86 | toJsonSchema: ({ type, schemas }) => { |
| 87 | invocations++ |
| 88 | assert.strictEqual(type, "string") |
| 89 | assert.strictEqual(schemas.length, 1) |
| 90 | assert.strictEqual(schemas[0].type, "number") |
| 91 | return { pattern: "^a" } |
| 92 | } |
| 93 | })) |
| 94 | |
| 95 | assert.deepStrictEqual(toCodecAnthropic(schema).jsonSchema, { |
| 96 | type: "string", |
| 97 | pattern: "^a" |
| 98 | }) |
| 99 | assert.strictEqual(invocations, 1) |
| 100 | }) |
| 101 | |
| 102 | it("compiles custom callbacks after structural transformations", () => { |
| 103 | let invocations = 0 |
| 104 | const schema = Schema.Record(Schema.String, Schema.Finite).check( |
| 105 | Schema.makeFilter<object>(() => true, { |
| 106 | representation: { |
| 107 | id: "test/ai/anthropic/structuralCompilerInput", |
| 108 | payload: null |
| 109 | }, |
| 110 | toJsonSchema: ({ type }) => { |
| 111 | invocations++ |
| 112 | assert.strictEqual(type, "array") |
| 113 | return { description: "compiled from the provider shape" } |
| 114 | } |
| 115 | }) |
| 116 | ) |
| 117 | |
| 118 | const jsonSchema = toCodecAnthropic(schema).jsonSchema |
| 119 | assert.strictEqual(jsonSchema.type, "array") |
| 120 | assert.strictEqual( |
| 121 | jsonSchema.description, |
| 122 | "Object encoded as array of [key, value] pairs. Apply object constraints to the decoded object and compiled from the provider shape" |
| 123 | ) |
| 124 | assert.strictEqual(invocations, 1) |
| 125 | }) |
| 126 | |
| 127 | it("keeps property names that match unsupported keywords", () => { |
| 128 | const unsupported = Schema.Any.check(Schema.makeFilter<unknown>(() => true, { |
| 129 | representation: { |
| 130 | id: "test/ai/anthropic/unsupportedPropertySchema", |
| 131 | payload: null |
| 132 | }, |
| 133 | toJsonSchema: () => ({ minLength: 2 }) |
| 134 | })) |
| 135 | |
| 136 | assert.deepStrictEqual(toCodecAnthropic(Schema.Struct({ minLength: unsupported })).jsonSchema, { |
| 137 | type: "object", |
| 138 | properties: { minLength: {} }, |
| 139 | required: ["minLength"], |
| 140 | additionalProperties: false |
| 141 | }) |
| 142 | }) |
| 143 | |
| 144 | it("keeps the original codec on the unchanged fast path", () => { |
| 145 | assert.strictEqual(toCodecAnthropic(Schema.String).codec, Schema.String) |
| 146 | }) |
| 147 | }) |