11import { Buffer } from "node:buffer"
2- import { Effect , Schema , Stream } from "effect"
2+ import { Effect , JsonSchema , Schema , Stream } from "effect"
33import * as Sse from "effect/unstable/encoding/Sse"
44import { Headers , HttpClientRequest } from "effect/unstable/http"
55import {
@@ -12,7 +12,8 @@ import {
1212 type TextPart ,
1313 type ToolResultPart ,
1414} from "../schema"
15- export { isRecord } from "../utils/record"
15+ import { isRecord } from "../utils/record"
16+ export { isRecord }
1617
1718export const Json = Schema . fromJsonString ( Schema . Unknown )
1819export const decodeJson = Schema . decodeUnknownSync ( Json )
@@ -21,6 +22,38 @@ export const JsonObject = Schema.Record(Schema.String, Schema.Unknown)
2122export const optionalArray = < const S extends Schema . Top > ( schema : S ) => Schema . optional ( Schema . Array ( schema ) )
2223export const optionalNull = < const S extends Schema . Top > ( schema : S ) => Schema . optional ( Schema . NullOr ( schema ) )
2324
25+ /** OpenAI function schemas require one flat object at the top level. */
26+ export const openAiToolInputSchema = ( schema : JsonSchema . JsonSchema ) : JsonSchema . JsonSchema => {
27+ const variants = Array . isArray ( schema . anyOf ) ? schema . anyOf . filter ( isRecord ) : [ ]
28+ const flattened = variants . length === 0
29+ ? { ...schema , type : "object" }
30+ : {
31+ ...Object . fromEntries ( Object . entries ( schema ) . filter ( ( [ key ] ) => key !== "anyOf" ) ) ,
32+ type : "object" ,
33+ properties : variants . reduce (
34+ ( properties , variant ) => ( { ...( isRecord ( variant . properties ) ? variant . properties : { } ) , ...properties } ) ,
35+ { } ,
36+ ) ,
37+ additionalProperties : false ,
38+ }
39+ const normalized = removeNullSchemas ( flattened )
40+ return isRecord ( normalized ) ? normalized : { type : "object" }
41+ }
42+
43+ const removeNullSchemas = ( value : unknown ) : unknown => {
44+ if ( Array . isArray ( value ) ) return value . map ( removeNullSchemas )
45+ if ( ! isRecord ( value ) ) return value
46+ const fields = Object . fromEntries (
47+ Object . entries ( value )
48+ . filter ( ( [ key ] ) => key !== "anyOf" )
49+ . map ( ( [ key , field ] ) => [ key , removeNullSchemas ( field ) ] ) ,
50+ )
51+ if ( ! Array . isArray ( value . anyOf ) ) return fields
52+ const variants = value . anyOf . filter ( ( variant ) => ! isRecord ( variant ) || variant . type !== "null" ) . map ( removeNullSchemas )
53+ if ( variants . length === 1 && isRecord ( variants [ 0 ] ) ) return { ...fields , ...variants [ 0 ] }
54+ return { ...fields , anyOf : variants }
55+ }
56+
2457/**
2558 * Streaming tool-call accumulator. Adapters that build a tool call across
2659 * multiple `tool-input-delta` chunks store the partial JSON input string here
0 commit comments