tazatechnology / openapi_spec

Dart based OpenAPI specification generator and parser
BSD 3-Clause "New" or "Revised" License
8 stars 5 forks source link

Closes #23 Use title to create inner schema names #24

Closed walsha2 closed 10 months ago

walsha2 commented 10 months ago

My only comment is to reconsider the title logic. With the current logic, there is no way to specify the desired class name of a nested schema (as it always concatenates the parent schema name + the inner schema title),

@davidmigloz the default schema naming has been reconsidered and the title property is now honored. Although, there are still checks to ensure that the schema name is unique, if it is not (due to spec duplication or user error) the generator will fallback to the longer unique name. Else you will get some very cryptic code generation errors that may be hard to trace due to the name conflicts.

Ultimately, these latest changes result in the following client code. I believe this is a lot closer to what you envisioned initially. See a few of the permutations below. I agree, this is much more developer friendly compared to the default logic (when no custom title is provided) seen in #21

EDIT: Example below also showcases deserialization logic for primitive unions added in #29

CreateCompletionRequest c;

c = CreateCompletionRequest(
  model: CompletionModel.enumeration(
    CompletionModelCatalog.davinci002,
  ),
  prompt: CompletionPrompt.string(
    'Hello world',
  ),
  stop: CompletionStop.string(
    'stop',
  ),
);

// Ensure serialization and deserialization work properly
print(c.toJson());
c = CreateCompletionRequest.fromJson(c.toJson());
print(c);

c = CreateCompletionRequest(
  model: CompletionModel.string(
    'custom model',
  ),
  prompt: CompletionPrompt.arrayString(
    ['hello', 'world'],
  ),
  stop: CompletionStop.arrayString(
    ['stop', 'end'],
  ),
);

// Ensure serialization and deserialization work properly
print(c.toJson());
c = CreateCompletionRequest.fromJson(c.toJson());
print(c);

YAML

Having said that, for the CreateCompletionRequest example we have been using, a few small mods to the spec title property of the parameters:

CreateCompletionRequest:
  type: object
  properties:
    model:
      title: 'CompletionModel'
      anyOf:
        - type: string
        - type: string
          title: 'CompletionModelCatalog'
          enum:
            [
              "babbage-002",
              "davinci-002",
              "gpt-3.5-turbo-instruct",
              "text-davinci-003",
              "text-davinci-002",
              "text-davinci-001",
              "code-davinci-002",
              "text-curie-001",
              "text-babbage-001",
              "text-ada-001",
            ]
    prompt:
      title: 'CompletionPrompt'
      nullable: true
      oneOf:
        - type: string
          default: ""
        - type: array
          items:
            type: string
            default: ""
        - type: array
          minItems: 1
          items:
            type: integer
        - type: array
          minItems: 1
          items:
            type: array
            minItems: 1
            items:
              type: integer
    stop:
      title: 'CompletionStop'
      default: null
      nullable: true
      oneOf:
        - type: string
          default: <|endoftext|>
          example: "\n"
          nullable: true
        - type: array
          minItems: 1
          maxItems: 4
          items:
            type: string
            example: '["\n"]'
  required:
    - model
    - prompt

Dart

// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: invalid_annotation_target
part of openai_schema;

// ==========================================
// CLASS: CreateCompletionRequest
// ==========================================

/// No Description
@freezed
class CreateCompletionRequest with _$CreateCompletionRequest {
  const CreateCompletionRequest._();

  /// Factory constructor for CreateCompletionRequest
  const factory CreateCompletionRequest({
    /// ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to see all of your available models, or see our [Model overview](/docs/models/overview) for descriptions of them.
    @_CompletionModelConverter() required CompletionModel model,

    /// The prompt(s) to generate completions for, encoded as a string, array of strings, array of tokens, or array of token arrays.
    ///
    /// Note that <|endoftext|> is the document separator that the model sees during training, so if a prompt is not specified the model will generate as if from the beginning of a new document.
    @_CompletionPromptConverter() required CompletionPrompt? prompt,

    /// Generates `best_of` completions server-side and returns the "best" (the one with the highest log probability per token). Results cannot be streamed.
    ///
    /// When used with `n`, `best_of` controls the number of candidate completions and `n` specifies how many to return – `best_of` must be greater than `n`.
    ///
    /// **Note:** Because this parameter generates many completions, it can quickly consume your token quota. Use carefully and ensure that you have reasonable settings for `max_tokens` and `stop`.
    @JsonKey(name: 'best_of', includeIfNull: false) @Default(1) int? bestOf,

    /// Echo back the prompt in addition to the completion
    @JsonKey(includeIfNull: false) @Default(false) bool? echo,

    /// Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.
    ///
    /// [See more information about frequency and presence penalties.](/docs/guides/gpt/parameter-details)
    @JsonKey(name: 'frequency_penalty', includeIfNull: false)
    @Default(0.0)
    double? frequencyPenalty,

    /// Modify the likelihood of specified tokens appearing in the completion.
    ///
    /// Accepts a json object that maps tokens (specified by their token ID in the GPT tokenizer) to an associated bias value from -100 to 100. You can use this [tokenizer tool](/tokenizer?view=bpe) (which works for both GPT-2 and GPT-3) to convert text to token IDs. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token.
    ///
    /// As an example, you can pass `{"50256": -100}` to prevent the <|endoftext|> token from being generated.
    @JsonKey(name: 'logit_bias', includeIfNull: false)
    Map<String, int>? logitBias,

    /// Include the log probabilities on the `logprobs` most likely tokens, as well the chosen tokens. For example, if `logprobs` is 5, the API will return a list of the 5 most likely tokens. The API will always return the `logprob` of the sampled token, so there may be up to `logprobs+1` elements in the response.
    ///
    /// The maximum value for `logprobs` is 5.
    @JsonKey(includeIfNull: false) int? logprobs,

    /// The maximum number of [tokens](/tokenizer) to generate in the completion.
    ///
    /// The token count of your prompt plus `max_tokens` cannot exceed the model's context length. [Example Python code](https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken) for counting tokens.
    @JsonKey(name: 'max_tokens', includeIfNull: false)
    @Default(16)
    int? maxTokens,

    /// How many completions to generate for each prompt.
    ///
    /// **Note:** Because this parameter generates many completions, it can quickly consume your token quota. Use carefully and ensure that you have reasonable settings for `max_tokens` and `stop`.
    @JsonKey(includeIfNull: false) @Default(1) int? n,

    /// Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.
    ///
    /// [See more information about frequency and presence penalties.](/docs/guides/gpt/parameter-details)
    @JsonKey(name: 'presence_penalty', includeIfNull: false)
    @Default(0.0)
    double? presencePenalty,

    /// Up to 4 sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence.
    @_CompletionStopConverter()
    @JsonKey(includeIfNull: false)
    CompletionStop? stop,

    /// Whether to stream back partial progress. If set, tokens will be sent as data-only [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) as they become available, with the stream terminated by a `data: [DONE]` message. [Example Python code](https://cookbook.openai.com/examples/how_to_stream_completions).
    @JsonKey(includeIfNull: false) @Default(false) bool? stream,

    /// The suffix that comes after a completion of inserted text.
    @JsonKey(includeIfNull: false) String? suffix,

    /// What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.
    ///
    /// We generally recommend altering this or `top_p` but not both.
    @JsonKey(includeIfNull: false) @Default(1.0) double? temperature,

    /// An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.
    ///
    /// We generally recommend altering this or `temperature` but not both.
    @JsonKey(name: 'top_p', includeIfNull: false) @Default(1.0) double? topP,

    /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices/end-user-ids).
    @JsonKey(includeIfNull: false) String? user,
  }) = _CreateCompletionRequest;

  /// Object construction from a JSON representation
  factory CreateCompletionRequest.fromJson(Map<String, dynamic> json) =>
      _$CreateCompletionRequestFromJson(json);

  /// List of all property names of schema
  static const List<String> propertyNames = [
    'model',
    'prompt',
    'best_of',
    'echo',
    'frequency_penalty',
    'logit_bias',
    'logprobs',
    'max_tokens',
    'n',
    'presence_penalty',
    'stop',
    'stream',
    'suffix',
    'temperature',
    'top_p',
    'user'
  ];

  /// Validation constants
  static const bestOfDefaultValue = 1;
  static const bestOfMinValue = 0;
  static const bestOfMaxValue = 20;
  static const frequencyPenaltyDefaultValue = 0.0;
  static const frequencyPenaltyMinValue = -2.0;
  static const frequencyPenaltyMaxValue = 2.0;
  static const logprobsMinValue = 0;
  static const logprobsMaxValue = 5;
  static const maxTokensDefaultValue = 16;
  static const maxTokensMinValue = 0;
  static const nDefaultValue = 1;
  static const nMinValue = 1;
  static const nMaxValue = 128;
  static const presencePenaltyDefaultValue = 0.0;
  static const presencePenaltyMinValue = -2.0;
  static const presencePenaltyMaxValue = 2.0;
  static const temperatureDefaultValue = 1.0;
  static const temperatureMinValue = 0.0;
  static const temperatureMaxValue = 2.0;
  static const topPDefaultValue = 1.0;
  static const topPMinValue = 0.0;
  static const topPMaxValue = 1.0;

  /// Perform validations on the schema property values
  String? validateSchema() {
    if (bestOf != null && bestOf! < bestOfMinValue) {
      return "The value of 'bestOf' cannot be < $bestOfMinValue";
    }
    if (bestOf != null && bestOf! > bestOfMaxValue) {
      return "The value of 'bestOf' cannot be > $bestOfMaxValue";
    }
    if (frequencyPenalty != null &&
        frequencyPenalty! < frequencyPenaltyMinValue) {
      return "The value of 'frequencyPenalty' cannot be < $frequencyPenaltyMinValue";
    }
    if (frequencyPenalty != null &&
        frequencyPenalty! > frequencyPenaltyMaxValue) {
      return "The value of 'frequencyPenalty' cannot be > $frequencyPenaltyMaxValue";
    }
    if (logprobs != null && logprobs! < logprobsMinValue) {
      return "The value of 'logprobs' cannot be < $logprobsMinValue";
    }
    if (logprobs != null && logprobs! > logprobsMaxValue) {
      return "The value of 'logprobs' cannot be > $logprobsMaxValue";
    }
    if (maxTokens != null && maxTokens! < maxTokensMinValue) {
      return "The value of 'maxTokens' cannot be < $maxTokensMinValue";
    }
    if (n != null && n! < nMinValue) {
      return "The value of 'n' cannot be < $nMinValue";
    }
    if (n != null && n! > nMaxValue) {
      return "The value of 'n' cannot be > $nMaxValue";
    }
    if (presencePenalty != null && presencePenalty! < presencePenaltyMinValue) {
      return "The value of 'presencePenalty' cannot be < $presencePenaltyMinValue";
    }
    if (presencePenalty != null && presencePenalty! > presencePenaltyMaxValue) {
      return "The value of 'presencePenalty' cannot be > $presencePenaltyMaxValue";
    }
    if (temperature != null && temperature! < temperatureMinValue) {
      return "The value of 'temperature' cannot be < $temperatureMinValue";
    }
    if (temperature != null && temperature! > temperatureMaxValue) {
      return "The value of 'temperature' cannot be > $temperatureMaxValue";
    }
    if (topP != null && topP! < topPMinValue) {
      return "The value of 'topP' cannot be < $topPMinValue";
    }
    if (topP != null && topP! > topPMaxValue) {
      return "The value of 'topP' cannot be > $topPMaxValue";
    }
    return null;
  }

  /// Map representation of object (not serialized)
  Map<String, dynamic> toMap() {
    return {
      'model': model,
      'prompt': prompt,
      'best_of': bestOf,
      'echo': echo,
      'frequency_penalty': frequencyPenalty,
      'logit_bias': logitBias,
      'logprobs': logprobs,
      'max_tokens': maxTokens,
      'n': n,
      'presence_penalty': presencePenalty,
      'stop': stop,
      'stream': stream,
      'suffix': suffix,
      'temperature': temperature,
      'top_p': topP,
      'user': user,
    };
  }
}

// ==========================================
// ENUM: CompletionModelCatalog
// ==========================================

/// No Description
enum CompletionModelCatalog {
  @JsonValue('babbage-002')
  babbage002,
  @JsonValue('davinci-002')
  davinci002,
  @JsonValue('gpt-3.5-turbo-instruct')
  gpt35TurboInstruct,
  @JsonValue('text-davinci-003')
  textDavinci003,
  @JsonValue('text-davinci-002')
  textDavinci002,
  @JsonValue('text-davinci-001')
  textDavinci001,
  @JsonValue('code-davinci-002')
  codeDavinci002,
  @JsonValue('text-curie-001')
  textCurie001,
  @JsonValue('text-babbage-001')
  textBabbage001,
  @JsonValue('text-ada-001')
  textAda001,
}

// ==========================================
// CLASS: CompletionModel
// ==========================================

/// ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to see all of your available models, or see our [Model overview](/docs/models/overview) for descriptions of them.
@freezed
sealed class CompletionModel with _$CompletionModel {
  const CompletionModel._();

  const factory CompletionModel.string(
    String value,
  ) = _UnionCompletionModelString;

  const factory CompletionModel.enumeration(
    CompletionModelCatalog value,
  ) = _UnionCompletionModelEnum;

  /// Object construction from a JSON representation
  factory CompletionModel.fromJson(Map<String, dynamic> json) =>
      _$CompletionModelFromJson(json);
}

/// Custom JSON converter for [CompletionModel]
class _CompletionModelConverter
    implements JsonConverter<CompletionModel, Object?> {
  const _CompletionModelConverter();

  @override
  CompletionModel fromJson(Object? data) {
    if (data is String &&
        _$CompletionModelCatalogEnumMap.values.contains(data)) {
      return CompletionModel.enumeration(
        _$CompletionModelCatalogEnumMap.keys.elementAt(
          _$CompletionModelCatalogEnumMap.values.toList().indexOf(data),
        ),
      );
    }
    if (data is String) {
      return CompletionModel.string(data);
    }
    throw Exception('Unexpected value for CompletionModel: $data');
  }

  @override
  Object? toJson(CompletionModel data) {
    return switch (data) {
      _UnionCompletionModelString(value: final v) => v,
      _UnionCompletionModelEnum(value: final v) =>
        _$CompletionModelCatalogEnumMap[v]!,
    };
  }
}
// ==========================================
// CLASS: CompletionPrompt
// ==========================================

/// The prompt(s) to generate completions for, encoded as a string, array of strings, array of tokens, or array of token arrays.
///
/// Note that <|endoftext|> is the document separator that the model sees during training, so if a prompt is not specified the model will generate as if from the beginning of a new document.
@freezed
sealed class CompletionPrompt with _$CompletionPrompt {
  const CompletionPrompt._();

  const factory CompletionPrompt.string(
    String value,
  ) = _UnionCompletionPromptString;

  const factory CompletionPrompt.arrayString(
    List<String> value,
  ) = _UnionCompletionPromptArrayString;

  const factory CompletionPrompt.arrayInteger(
    List<int> value,
  ) = _UnionCompletionPromptArrayInteger;

  const factory CompletionPrompt.array(
    List<List<int>> value,
  ) = _UnionCompletionPromptArray;

  /// Object construction from a JSON representation
  factory CompletionPrompt.fromJson(Map<String, dynamic> json) =>
      _$CompletionPromptFromJson(json);
}

/// Custom JSON converter for [CompletionPrompt]
class _CompletionPromptConverter
    implements JsonConverter<CompletionPrompt?, Object?> {
  const _CompletionPromptConverter();

  @override
  CompletionPrompt? fromJson(Object? data) {
    if (data == null) {
      return null;
    }
    if (data is String) {
      return CompletionPrompt.string(data);
    }
    if (data is List<String>) {
      return CompletionPrompt.arrayString(data);
    }
    if (data is List<int>) {
      return CompletionPrompt.arrayInteger(data);
    }
    if (data is List<List<int>>) {
      return CompletionPrompt.array(data);
    }
    return CompletionPrompt.string('<|endoftext|>');
  }

  @override
  Object? toJson(CompletionPrompt? data) {
    return switch (data) {
      _UnionCompletionPromptString(value: final v) => v,
      _UnionCompletionPromptArrayString(value: final v) => v,
      _UnionCompletionPromptArrayInteger(value: final v) => v,
      _UnionCompletionPromptArray(value: final v) => v,
      null => null,
    };
  }
}
// ==========================================
// CLASS: CompletionStop
// ==========================================

/// Up to 4 sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence.
@freezed
sealed class CompletionStop with _$CompletionStop {
  const CompletionStop._();

  const factory CompletionStop.string(
    String value,
  ) = _UnionCompletionStopString;

  const factory CompletionStop.arrayString(
    List<String> value,
  ) = _UnionCompletionStopArrayString;

  /// Object construction from a JSON representation
  factory CompletionStop.fromJson(Map<String, dynamic> json) =>
      _$CompletionStopFromJson(json);
}

/// Custom JSON converter for [CompletionStop]
class _CompletionStopConverter
    implements JsonConverter<CompletionStop?, Object?> {
  const _CompletionStopConverter();

  @override
  CompletionStop? fromJson(Object? data) {
    if (data == null) {
      return null;
    }
    if (data is String) {
      return CompletionStop.string(data);
    }
    if (data is List<String>) {
      return CompletionStop.arrayString(data);
    }
    throw Exception('Unexpected value for CompletionStop: $data');
  }

  @override
  Object? toJson(CompletionStop? data) {
    return switch (data) {
      _UnionCompletionStopString(value: final v) => v,
      _UnionCompletionStopArrayString(value: final v) => v,
      null => null,
    };
  }
}
davidmigloz commented 10 months ago

This is awesome! And your implementation is still as robust as it used to. Thanks for being so fast, I'll give it a try 🚀