Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add support for latest openai structured outputs types #48

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

transitive-bullshit
Copy link
Collaborator

@transitive-bullshit transitive-bullshit commented Aug 9, 2024

This PR adds support for latest OpenAI's structured outputs release.

  • updates openai-fetch to latest major Update ChatMessage type with 'refusal' and 'content' is now optional openai-fetch#46
  • adds a Msg.Refusal message type
  • we now have to differentiate slightly between input ChatMessage types where content and refusal are both optional and response ChatMessage types where content and refusal are apparently both required
  • adds support to AIFunctionSpec, createAIFunction, and createAIExtractFunction for OpenAI's new strict JSON schema support

Copy link

vercel bot commented Aug 9, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
dexter ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 9, 2024 5:32am

@transitive-bullshit transitive-bullshit marked this pull request as draft August 9, 2024 05:03
Comment on lines +107 to +111
if (Msg.isRefusal(lastMessage)) {
throw new AbortError(lastMessage.refusal);
} else if (shouldBreakLoop(lastMessage)) {
const content = await Promise.resolve(
validateContent(lastMessage.content)
validateContent(lastMessage.content!)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the pattern:

if (Msg.isRefusal(message)) { ... } else { const content = message.content! }

is unfortunately going to be a common one going forwards I think

Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't Msg.isRefusal() narrow the type to the point where TypeScript knows message.content: string?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the else block here doesn't guarantee that content exists

role: 'assistant';
name?: string;
refusal: string;
};
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

refusal messages apparently also always have content: null

also, normal assistant messages apparently also always have refusal: null

…IFunction, zodToJsonSchema, and createAIExtractFunction
@@ -58,7 +58,6 @@
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
"prebuild": "rimraf dist",
"predocs": "rimraf docs/pages/docs",
"prepare": "pnpm run build",
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

it's weird to have a pnpm add fail because the build isn't working. I don't think prepare scripts are a good fit for this

@@ -5,6 +5,7 @@ import { ChatModel } from './chat.js';
const FAKE_RESPONSE: Model.Chat.Response = {
message: {
content: 'Hi from fake AI',
refusal: null,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

boooo

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this is required for the openai types to match up w/ response chat messages

Copy link
Contributor

Choose a reason for hiding this comment

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

dexter may live on forever just as a cleanup layer on top of openai's god awful schema design lol

Copy link
Contributor

@rileytomasek rileytomasek left a comment

Choose a reason for hiding this comment

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

I need to think a little more about this one because it's doing to have a negative impact on the DX for a relatively small gain.

It might be worth refusing to acknowledge the refusal message type... Don't add it to the Msg family and instead handle refusals by parsing the response and throwing an error if there is a refusal. Then we can handle the (hopefully edge case...) where a refusal is sent without making the DX of these message objects even worse.

PS: I thought OpenAI fired all the tedious people??

@rileytomasek
Copy link
Contributor

@transitive-bullshit what do you think of my last comment?

@rileytomasek
Copy link
Contributor

@transitive-bullshit was thinking more about this... what do you think of creating a custom error for refusals and throwing them as an error? that way we could type the api return type as Msg | RefusalMsg and then parse the response and throw an error for RefusalMsg, which would narrow the type back to Msg for downstream code?

@rileytomasek
Copy link
Contributor

@transitive-bullshit #51 handles the update and refusal responses by throwing. if we can make that work, adding a dedicated function for structured output calls should be simple.

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.

2 participants