Skip to content

feat(evals): add OpenEval dataset import and export helpers (fix #3549) - #3619

Open
SparshGarg999 wants to merge 3 commits into
openai:mainfrom
SparshGarg999:feat-openeval-dataset-converter
Open

feat(evals): add OpenEval dataset import and export helpers (fix #3549)#3619
SparshGarg999 wants to merge 3 commits into
openai:mainfrom
SparshGarg999:feat-openeval-dataset-converter

Conversation

@SparshGarg999

Copy link
Copy Markdown

Fixes #3549

Summary

Add from_openeval and to_openeval dataset conversion utilities in openai.types.evals to support importing and exporting portable LLM evaluation datasets matching the OpenEval standard format.

  • from_openeval(item: OpenEvalItem) converts an OpenEval format dataset item into OpenAI Chat Completion message parameters.
  • to_openeval(messages, ...) exports OpenAI Chat Completion messages and metadata into an OpenEval format item.

How was this tested?

  • Added unit tests in tests/test_openeval.py testing serialization and deserialization of OpenEval items.

@SparshGarg999
SparshGarg999 requested a review from a team as a code owner August 14, 2026 17:19

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f4151d77f4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1 to 5
from .openeval import (
OpenEvalItem as OpenEvalItem,
from_openeval as from_openeval,
to_openeval as to_openeval,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve existing evals type exports

This replacement leaves openai.types.evals exporting only the new OpenEval helpers, so documented and in-repo imports such as from openai.types.evals import RunCreateResponse in tests/api_resources/evals/test_runs.py now fail for users of the evals runs API. Please add the new helpers without removing the generated eval response/param exports.

AGENTS.md reference: AGENTS.md:L5-L8

Useful? React with 👍 / 👎.

Comment on lines +1 to 5
from .openeval import (
OpenEvalItem as OpenEvalItem,
from_openeval as from_openeval,
to_openeval as to_openeval,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve existing evals type exports

This replacement leaves openai.types.evals exporting only the new OpenEval helpers, so documented and in-repo imports such as from openai.types.evals import RunCreateResponse in tests/api_resources/evals/test_runs.py now fail for users of the evals runs API. Please add the new helpers without removing the generated eval response/param exports.

Useful? React with 👍 / 👎.

Comment thread src/openai/types/evals/openeval.py Outdated
Convert an OpenEval dataset item into OpenAI Chat Completion messages format.
"""
messages: List[Dict[str, Any]] = []
for msg in item.get("input", []):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept scalar OpenEval inputs

OpenEval/EvalPort JSONL rows commonly use scalar input values such as {'input': 'What is 2+2?'}, but this loop assumes every input entry is a message dict. In that valid case Python iterates the prompt characters and msg.get(...) raises AttributeError, so the import helper cannot read standard single-prompt datasets; please branch on str or turn arrays before treating entries as dicts.

Useful? React with 👍 / 👎.

Comment thread src/openai/types/evals/openeval.py Outdated
{"role": str(msg.get("role", "user")), "content": str(msg.get("content", ""))}
for msg in messages
]
item: OpenEvalItem = {"input": input_messages}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Emit grader references in exports

When callers use to_openeval() to write a dataset, the returned object starts as only {'input': ...} and the function has no way to add graders, which are required on OpenEval/EvalPort TestCase rows. The resulting JSONL cannot be consumed or validated as OpenEval unless callers mutate every item afterward, so the helper should accept and emit grader references or export a suite with them.

Useful? React with 👍 / 👎.

Comment thread src/openai/types/evals/openeval.py Outdated
result: Dict[str, Any] = {"messages": messages}
if item.get("id"):
result["id"] = item["id"]
if item.get("expected_output"):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve empty expected outputs

When a dataset case expects an empty string, for example asserting that the model should produce no text, this truthiness check drops expected_output entirely. Downstream graders then see no reference output rather than ''; match the export path and preserve values that are present but falsy.

Useful? React with 👍 / 👎.

Comment thread src/openai/types/evals/openeval.py Outdated
Export OpenAI Chat Completion messages and metadata into OpenEval dataset item format.
"""
input_messages: List[OpenEvalMessage] = [
{"role": str(msg.get("role", "user")), "content": str(msg.get("content", ""))}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve structured chat message content

When exporting multimodal Chat Completions messages, content can be a list of content-part dictionaries, but this casts it through str(...) and writes a Python repr instead of preserving the structured JSON. Those exported eval rows cannot be round-tripped back into Chat Completions or consumed by tools expecting real content parts, so keep non-string content as structured data rather than stringifying it.

Useful? React with 👍 / 👎.

@adhabnr-ux

Copy link
Copy Markdown

Hey @SparshGarg999 — great to see someone build against the real spec here, thank you for picking up #3549.

One thing worth flagging before this lands: the OpenEvalItem shape in src/openai/types/evals/openeval.py doesn't yet produce a document that validates against the actual EvalPort TestCase schema (spec/schemas/testcase.json). Two specific gaps:

  • id and graders are both required there ("required": ["id", "input", "graders"]). OpenEvalItem has id as Optional[str] and has no graders field at all — a TestCase with no grader (either a grader-id string or an inline grader object) won't pass validate_suite().
  • input is typed oneOf: [{"type": "string", "minLength": 1}, {"type": "array", "items": {"type": "string"}, "minItems": 1}] — a string, or an array of strings — not an array of chat-message objects. The OpenEvalMessage-shaped {role, content} list is actually the OpenAI side of the conversion, i.e. what from_openeval() should produce, not the shape to_openeval() should treat as already-valid OpenEval input.

None of this needs a rewrite, just a schema-alignment pass on OpenEvalItem and the two functions. Happy to help either way:

Want me to sketch a corrected OpenEvalItem/to_openeval/from_openeval diff against your branch, or would you rather take it from here with the schema link above? Either way, glad to see OpenEval getting first-class support in the OpenAI SDK.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9e806e37fd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/openai/types/evals/openeval.py Outdated
Export OpenAI Chat Completion messages and metadata into OpenEval dataset item format.
"""
input_messages: List[OpenEvalMessage] = [
{"role": str(msg.get("role", "user")), "content": str(msg.get("content", ""))}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve tool-call fields when exporting conversations

When the chat history contains tool/function-call messages, this export silently keeps only role and content. For example, a {"role": "tool", "tool_call_id": "call_1", ...} message round-trips through to_openeval()/from_openeval() without tool_call_id, but the SDK's ChatCompletionToolMessageParam requires that field, so exported agent/tool eval datasets produce messages that cannot be sent back to Chat Completions. Please preserve the remaining Chat Completion message fields or reject unsupported roles instead of stripping them.

Useful? React with 👍 / 👎.

Comment thread src/openai/types/evals/openeval.py Outdated
Export OpenAI Chat Completion messages and metadata into OpenEval dataset item format.
"""
input_messages: List[OpenEvalMessage] = [
{"role": str(msg.get("role", "user")), "content": str(msg.get("content", ""))}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Emit schema-valid OpenEval inputs

For any normal chat message list, to_openeval() writes input as an array of {role, content} objects. The OpenEval/EvalPort TestCase schema only accepts input as a single string or an array of strings, so even the new test_to_openeval_conversion fixture would fail validation before graders are considered. Export the prompt text in the standard shape, or use a documented extension, rather than Chat Completions message objects.

Useful? React with 👍 / 👎.

@SparshGarg999

Copy link
Copy Markdown
Author

Hi @adhabnr-ux,

Thank you so much for the detailed feedback and for catching these schema discrepancies! That's super helpful.

I would really appreciate it if you could sketch a diff for OpenEvalItem, to_openeval, and from_openeval against this branch! Having it aligned directly with the EvalPort TestCase schema (id, graders, and input as string or list of strings) will ensure true spec validity.

In the meantime, I'll update src/openai/types/evals/__init__.py to ensure existing evals type exports are preserved and update the test assertions.

Thanks again for your guidance!

@adhabnr-ux

Copy link
Copy Markdown

Here's a concrete sketch. I don't have write access to your branch, so this is a full replacement for src/openai/types/evals/openeval.py you can paste in and adjust — plus the reasoning behind each change, since a couple of these aren't just "add a field."

# File generated for OpenEval dataset import/export support.

from __future__ import annotations

import uuid
from typing import Any, Dict, List, Optional, Union
from typing_extensions import Required, TypedDict

__all__ = ["OpenEvalItem", "OpenEvalGrader", "from_openeval", "to_openeval"]


class OpenEvalMessage(TypedDict, total=False):
    role: str
    content: str


class OpenEvalGrader(TypedDict, total=False):
    """Inline grader object -- see spec/schemas/grader.json. Most callers
    will instead pass a bare grader-id string in OpenEvalItem['graders']."""

    id: Required[str]
    type: Required[str]
    params: Dict[str, Any]


class OpenEvalItem(TypedDict, total=False):
    # id and graders are REQUIRED by spec/schemas/testcase.json
    # ("required": ["id", "input", "graders"]) -- not optional.
    id: Required[str]
    input: Required[Union[str, List[str]]]
    graders: Required[List[Union[str, OpenEvalGrader]]]
    expected_output: Optional[str]
    metadata: Optional[Dict[str, Any]]


def from_openeval(item: OpenEvalItem) -> Dict[str, Any]:
    """
    Convert a spec-valid OpenEval TestCase into OpenAI Chat Completion
    messages format.

    ``item["input"]`` is a string or an array of strings per the EvalPort
    TestCase schema, not an array of {role, content} objects -- that chat
    shape is OpenAI's own native format, produced by to_openeval() below,
    not something to_openeval()'s *caller* is expected to hand you.

    If this item carries the original chat messages this adapter itself
    exported (see to_openeval()), they're restored losslessly from
    ``metadata["openai"]["messages"]``. Otherwise -- e.g. a hand-authored
    TestCase, or one from a different tool -- every input string is
    reconstructed as a single "user" message, the same fallback every other
    adapter in the EvalPort ecosystem uses for a grader/tool type it doesn't
    recognize.
    """
    metadata = item.get("metadata") or {}
    openai_meta = metadata.get("openai") or {}

    if "messages" in openai_meta:
        messages: List[Dict[str, Any]] = list(openai_meta["messages"])
    else:
        raw_input = item.get("input", "")
        input_strings = [raw_input] if isinstance(raw_input, str) else list(raw_input)
        messages = [{"role": "user", "content": s} for s in input_strings]

    result: Dict[str, Any] = {"messages": messages}
    if item.get("id"):
        result["id"] = item["id"]
    if item.get("expected_output"):
        result["expected_output"] = item["expected_output"]
    if metadata:
        result["metadata"] = metadata
    return result


def to_openeval(
    messages: List[Dict[str, Any]],
    graders: List[Union[str, OpenEvalGrader]],
    id: Optional[str] = None,
    expected_output: Optional[str] = None,
    metadata: Optional[Dict[str, Any]] = None,
) -> OpenEvalItem:
    """
    Export OpenAI Chat Completion messages into a spec-valid OpenEval
    TestCase (spec/schemas/testcase.json).

    Args:
        messages: OpenAI chat messages ({role, content} dicts).
        graders: REQUIRED -- TestCase.graders must have >=1 entry per spec.
            Pass grader-id strings (referencing graders already defined on
            the enclosing Suite) or inline grader objects.
        id: TestCase.id is required by spec; if omitted, a uuid4 is
            generated so the output always validates.
        expected_output, metadata: as before.

    ``input`` is built as one string per message ("{role}: {content}"),
    satisfying the schema's string-or-array-of-strings requirement. The
    *original* messages are additionally preserved verbatim under
    metadata["openai"]["messages"] so from_openeval() can reconstruct the
    exact role/content structure on import instead of collapsing everything
    to "user" turns -- "openai.*" following the same reserved-namespace
    convention every adapter in the EvalPort ecosystem uses ("openeval.*"
    is the only prefix EvalPort itself reserves).
    """
    input_strings = [
        f"{msg.get('role', 'user')}: {msg.get('content', '')}" for msg in messages
    ]

    merged_metadata: Dict[str, Any] = dict(metadata or {})
    merged_metadata["openai"] = {
        **merged_metadata.get("openai", {}),
        "messages": [
            {"role": str(m.get("role", "user")), "content": str(m.get("content", ""))}
            for m in messages
        ],
    }

    item: OpenEvalItem = {
        "id": id or str(uuid.uuid4()),
        "input": input_strings if len(input_strings) != 1 else input_strings[0],
        "graders": graders,
        "metadata": merged_metadata,
    }
    if expected_output is not None:
        item["expected_output"] = expected_output
    return item

What changed and why, against spec/schemas/testcase.json:

  1. id and graders are now required, matching "required": ["id", "input", "graders"]. to_openeval() generates a uuid4 when the caller doesn't supply an id (so output is always valid), and takes graders as a required parameter -- there's no honest default grader to synthesize on your side, since the caller is the one who knows what should grade this test case.
  2. input is now str or List[str], matching the schema's oneOf: [string, array-of-strings]. The chat-message shape ({role, content}) moves to being OpenAI's native representation, converted to/from the spec's string-array shape at the boundary -- with the original messages preserved verbatim in metadata["openai"]["messages"] so nothing is lost on to_openeval()from_openeval() round trips through this library specifically (a TestCase produced by some other tool won't have that key, so from_openeval() falls back to treating each input string as one "user" turn, same as any adapter handles data it didn't produce itself).
  3. Added OpenEvalGrader as a minimal TypedDict for the inline-grader-object half of graders' oneOf (bare id string, or {id, type, params} -- spec/schemas/grader.json), so a caller passing inline graders gets type-checking too.

I did not add JSON Schema validation as a runtime dependency here (evalport-sdk on PyPI ships the real openeval.validate.validate_suite() you could wire into tests/test_openeval.py as an assertion, mentioned in my last comment) -- figured that's your call on whether openai-python wants a new runtime dependency for this, versus just matching the shape by hand as this sketch does. Happy to also sketch what that test would look like if useful.

@adhabnr-ux

Copy link
Copy Markdown

One more piece to close the loop on, since Codex's review flagged it as P1 and my last sketch only touched openeval.py, not __init__.py: the current src/openai/types/evals/__init__.py on this branch drops every generated export the file originally had (EvalAPIError, RunListParams, RunCreateParams, RunListResponse, RunCancelResponse, RunCreateResponse, RunDeleteResponse, RunRetrieveResponse, CreateEvalJSONLRunDataSource, CreateEvalCompletionsRunDataSource, and their *Param variants), replacing them with only the three new OpenEval names. That breaks any existing import of those types (Codex's example: tests/api_resources/evals/test_runs.py imports RunCreateResponse from there).

Here's the fix — the original generated content with the OpenEval import appended, not swapped in:

# File generated from our OpenAPI spec by Castiron. See CONTRIBUTING.md for details.

from __future__ import annotations

from .eval_api_error import EvalAPIError as EvalAPIError
from .run_list_params import RunListParams as RunListParams
from .run_create_params import RunCreateParams as RunCreateParams
from .run_list_response import RunListResponse as RunListResponse
from .run_cancel_response import RunCancelResponse as RunCancelResponse
from .run_create_response import RunCreateResponse as RunCreateResponse
from .run_delete_response import RunDeleteResponse as RunDeleteResponse
from .run_retrieve_response import RunRetrieveResponse as RunRetrieveResponse
from .create_eval_jsonl_run_data_source import CreateEvalJSONLRunDataSource as CreateEvalJSONLRunDataSource
from .create_eval_completions_run_data_source import (
    CreateEvalCompletionsRunDataSource as CreateEvalCompletionsRunDataSource,
)
from .create_eval_jsonl_run_data_source_param import (
    CreateEvalJSONLRunDataSourceParam as CreateEvalJSONLRunDataSourceParam,
)
from .create_eval_completions_run_data_source_param import (
    CreateEvalCompletionsRunDataSourceParam as CreateEvalCompletionsRunDataSourceParam,
)
from .openeval import (
    OpenEvalItem as OpenEvalItem,
    OpenEvalGrader as OpenEvalGrader,
    from_openeval as from_openeval,
    to_openeval as to_openeval,
)

(OpenEvalGrader only needed if you take the graders-required version from my earlier sketch — drop that line if you go a different route on the required-graders point.)

Also worth noting: the two other Codex threads on the current openeval.py — scalar input handling and structured/tool-call message content — are both addressed by the sketch I posted above, since it branches on str vs. list for input and stores the original messages verbatim in metadata["openai"]["messages"] rather than re-deriving a lossy {role, content}-only shape. So incorporating that sketch should clear those alongside the schema-shape ones.

No urgency on my end — happy to review again whenever you've had a chance to work through these, or if it's easier for you to just take the diffs as-is and adjust from there, that works too.

@SparshGarg999

Copy link
Copy Markdown
Author

Thanks @adhabnr-ux! I have pushed updates addressing all the feedback:

  1. EvalPort TestCase Schema Alignment:
    • OpenEvalItem now conforms to the spec/schemas/testcase.json schema (id, input, and graders are all required fields).
    • Added OpenEvalGrader TypedDict for typed inline grader objects (id, type, params).
    • to_openeval() requires graders, auto-generates a uuid4 if id is omitted, formats single messages as scalar strings and multi-turn conversations as List[str], and preserves original messages verbatim under metadata["openai"]["messages"] for lossless round-tripping.
    • Preserved empty string expected_output (e.g., "") across both to_openeval and from_openeval.
  2. Preserved Generated Evals Type Exports:
    • Restored all generated response and parameter types in src/openai/types/evals/__init__.py while exporting the new OpenEval helpers.
  3. Tests:
    • Added test coverage in tests/test_openeval.py for scalar string inputs, multi-message lists, lossless metadata round-trips with tool calls and multimodal contents, inline grader definitions, auto-generated IDs, and empty expected_output values. All tests and type checks (pyright, mypy, ruff) are passing cleanly.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2525c603f4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

input_strings = [raw_input] if isinstance(raw_input, str) else list(raw_input)
messages = [{"role": "user", "content": s} for s in input_strings]

result: Dict[str, Any] = {"messages": messages}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve OpenEval context fields on import

When an OpenEval TestCase uses schema-valid fields such as context, retrieval_context, tools_called, or expected_tools (common for RAG and agent evals), from_openeval() initializes the result with only messages and later adds just id, expected_output, and metadata, so those inputs are silently dropped. The imported item can no longer provide retrieved docs or expected tool calls to graders or be re-exported faithfully; carry the remaining TestCase fields through, or explicitly encode context into the returned messages, instead of discarding them.

Useful? React with 👍 / 👎.

Comment on lines +141 to +142
"input": input_strings if len(input_strings) != 1 else input_strings[0],
"graders": graders,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject empty OpenEval inputs

If callers pass dynamically built empty messages or graders lists, this still returns an item with input: [] and/or graders: []; those violate the TestCase requirements that both the prompt array and grader list have at least one entry, despite the helper documenting that it exports a spec-valid TestCase. Please fail fast for empty lists before constructing the item so users do not write JSONL rows that downstream OpenEval validators reject.

Useful? React with 👍 / 👎.

Comment on lines +21 to +23
id: Required[str]
type: Required[str]
params: Dict[str, Any]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow schema-valid inline grader fields

Inline graders can legally carry fields such as weight and description, but the exported OpenEvalGrader TypedDict only exposes id, type, and params. Users who type their inline graders with the SDK therefore get type errors for schema-valid grader definitions before passing them to to_openeval(), even though the helper accepts and preserves those fields at runtime; include the supported optional fields in this public type.

Useful? React with 👍 / 👎.

@adhabnr-ux

Copy link
Copy Markdown

Verified this locally rather than just eyeballing the diff: pulled src/openai/types/evals/openeval.py and tests/test_openeval.py as they are on this branch right now, ran them standalone (no openai package dependency needed for the module itself) against evalport-sdk 1.0.0 from PyPI, and validated the actual output with the real openeval.validate.validate_test_case() / validate_suite():

  • Single-message, multi-message, inline-grader, and auto-generated-id cases from your test file all produce TestCase documents where validate_test_case(...).valid == True, zero schema errors.
  • Wrapped one as a full suite (with a matching grader-1: exact_match definition) and ran validate_suite() — also valid.
  • Reran the round-trip case (to_openevalfrom_openeval) — id, messages, and expected_output all come back exactly as they went in.

So this is genuinely spec-valid now, not just structurally similar — nice work threading the metadata["openai"].messages lossless round-trip in on top of the schema fix, that's a cleaner solve than what I sketched (I hadn't handled non-dict message entries or nested content like the tool-call/multimodal cases your tests now cover). Nothing further from me on the EvalPort-alignment side — this looks ready from that angle. Thanks for taking the extra pass on the __init__.py export regression too.

@adhabnr-ux

Copy link
Copy Markdown

Thanks for the quick turnaround, @SparshGarg999 — I pulled openeval.py from commit 2525c60 and ran it against the real evalport-sdk validator (openeval.validate.validate_suite, from PyPI, not a mock):

SUITE VALID: True

wrapping two to_openeval()-produced TestCases (single-message and multi-message/list-input) inside a full Suite with a real grader object — it validates cleanly end-to-end now. Specifically confirms: id/input/graders are present and correctly typed per spec/schemas/testcase.json (string-or-array-of-strings for input, non-empty graders), the required-field handling (id auto-generation, graders required not optional) matches spec, and the round-trip through metadata["openai"]["messages"] preserves the original chat structure losslessly on from_openeval(), which is exactly the right pattern for tool-specific data that doesn't map onto EvalPort's schema (matches the metadata["<tool>"] convention the other framework adapters in the EvalPort ecosystem use).

This looks correct and spec-valid to me. Nice work closing out the schema gaps from the earlier feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Proposal] OpenEval Import/Export Support

2 participants