package chatter import ( "context" "crypto/tls" "encoding/json" "fmt" "net/http" "reflect" "github.com/sashabaranov/go-openai" "github.com/sashabaranov/go-openai/jsonschema" ) type OpenAIAdapter struct { client *openai.Client model string } func NewOpenAIAdapter(apiKey string, model string, baseURL string) *OpenAIAdapter { config := openai.DefaultConfig(apiKey) tr := &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, // Bypasses the "not standards compliant" error }, } config.HTTPClient = &http.Client{Transport: tr} if baseURL != "" { config.BaseURL = baseURL } if baseURL != "" { config.BaseURL = baseURL } return &OpenAIAdapter{ client: openai.NewClientWithConfig(config), model: model, } } func (a *OpenAIAdapter) Generate(ctx context.Context, messages []Message) (string, error) { var chatMsgs []openai.ChatCompletionMessage for _, m := range messages { chatMsgs = append(chatMsgs, openai.ChatCompletionMessage{ Role: string(m.Role), Content: m.Content, }) } resp, err := a.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ Model: a.model, Messages: chatMsgs, }) if err != nil { return "", err } return resp.Choices[0].Message.Content, nil } func (a *OpenAIAdapter) GenerateStructured(ctx context.Context, messages []Message, target any) error { val := reflect.ValueOf(target) if val.Kind() != reflect.Ptr { return fmt.Errorf("target must be a pointer") } elem := val.Elem() var schemaType reflect.Type isSlice := elem.Kind() == reflect.Slice // 1. Wrap slices in an object because OpenAI requires a root object if isSlice { schemaType = reflect.StructOf([]reflect.StructField{ { Name: "Items", Type: elem.Type(), Tag: `json:"items"`, }, }) } else { schemaType = elem.Type() } // 2. Build the Schema Map schemaObj := a.reflectTypeToSchema(schemaType) // 3. Convert to json.RawMessage to satisfy the json.Marshaler interface schemaBytes, err := json.Marshal(schemaObj) if err != nil { return fmt.Errorf("failed to marshal schema: %w", err) } var chatMsgs []openai.ChatCompletionMessage for _, m := range messages { chatMsgs = append(chatMsgs, openai.ChatCompletionMessage{ Role: string(m.Role), Content: m.Content, }) } // 4. Send Request req := openai.ChatCompletionRequest{ Model: a.model, Messages: chatMsgs, ResponseFormat: &openai.ChatCompletionResponseFormat{ Type: openai.ChatCompletionResponseFormatTypeJSONSchema, JSONSchema: &openai.ChatCompletionResponseFormatJSONSchema{ Name: "output_schema", Strict: true, Schema: json.RawMessage(schemaBytes), }, }, } resp, err := a.client.CreateChatCompletion(ctx, req) if err != nil { return err } content := resp.Choices[0].Message.Content // 5. Unmarshal and Unwrap if necessary if isSlice { temp := struct { Items json.RawMessage `json:"items"` }{} if err := json.Unmarshal([]byte(content), &temp); err != nil { return err } return json.Unmarshal(temp.Items, target) } return json.Unmarshal([]byte(content), target) } func (a *OpenAIAdapter) reflectTypeToSchema(t reflect.Type) jsonschema.Definition { for t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.Struct: def := jsonschema.Definition{ Type: jsonschema.Object, Properties: make(map[string]jsonschema.Definition), AdditionalProperties: false, Required: []string{}, } for i := 0; i < t.NumField(); i++ { field := t.Field(i) if !field.IsExported() { continue } name := field.Tag.Get("json") if name == "" || name == "-" { name = field.Name } def.Properties[name] = a.reflectTypeToSchema(field.Type) def.Required = append(def.Required, name) } return def case reflect.Slice, reflect.Array: items := a.reflectTypeToSchema(t.Elem()) return jsonschema.Definition{ Type: jsonschema.Array, Items: &items, } case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return jsonschema.Definition{Type: jsonschema.Integer} case reflect.Float32, reflect.Float64: return jsonschema.Definition{Type: jsonschema.Number} case reflect.Bool: return jsonschema.Definition{Type: jsonschema.Boolean} default: return jsonschema.Definition{Type: jsonschema.String} } } func (a *OpenAIAdapter) GetProviderName() string { return "OpenAI (" + a.model + ")" }