Hi everyone, I was trying to write a nim client for the openai api but I ran into the following problem when parsing the json:
#example code that tests if the client is functioning properly
import nim_openai/[client]
import httpclient, json
template Json(body: untyped): untyped = `%*`(body)
let
env = loadEnvFile(".env")
api_key = env.get("API_KEY")
openai = newOpenAiClient(api_key = api_key)
let baz = Json {"image": "pic.png", "mask": "pic.png", "prompt": "A Nice Tesla For Asiwaju", "n": 2,
"size": "512x512"}
let foo = openai.createImageEdit(baz)
echo foo.body()
The code above when compiled with
nim c -r -d:ssl <filename>.nim
results in this reponse from openai:
{
"error": {
"code": null,
"message": "'\"512x512\"' is not one of ['256x256', '512x512', '1024x1024'] - 'size'",
"param": null,
"type": "invalid_request_error"
}
}
I was confused why this happened and asked for help on twitter which led to me finding out that if I changed lines 686 - 691 of the nim std/json from this:
proc escapeJson*(s: string; result: var string) =
## Converts a string `s` to its JSON representation with quotes.
## Appends to `result`.
result.add("\"")
escapeJsonUnquoted(s, result)
result.add("\"")
to this:
proc escapeJson*(s: string; result: var string) =
## Converts a string `s` to its JSON representation with quotes.
## Appends to `result`.
if s.contains("x"):
escapeJsonUnquoted(s, result)
else:
result.add("\"")
escapeJsonUnquoted(s, result)
result.add("\"")
The openai invalid request error message goes away. I dont know if I am the one doing something wrong or if the escapeJson implementation does not cover some edgecases? Please what steps should I take from here?The bug seems to be in this line of yours multipartBody[key] = $verifiedBody[key]
It should be multipartBody[key] = verifiedBody[key]. Maybe, I don't know.
The bug seems to be in this line of yours multipartBody[key] = $verifiedBody[key]
It should be multipartBody[key] = verifiedBody[key]. Maybe, I don't know.
The thing is that verifiedBody is a JsonNode and multipart[key] expects to be set to a string. And setting a jsonNode to a string calls toUgly (on line 820 of std/json) which in turn calls escapeJson(on line 729) if the jsonNode kind is a Jstring and the JsonNode is not unquoted.
setting a jsonNode to a string calls toUgly (on line 820 of std/json) which in turn calls escapeJson(on > line 729) if the jsonNode kind is a Jstring and the JsonNode is not unquoted.
which leads me back to the esacpeJson proc
I think this is a bug in your create multi part form: https://github.com/Uzo2005/nim-openai/blob/main/nim-openai/src/nim_openai/client.nim#L197-L208
OpenAI api wants everything int its part. See: https://platform.openai.com/docs/api-reference/images/create-edit
I think you are creating a single entry for the entire json line. But each field in the json needs its own part entry.
See my multi part encoder here: https://github.com/treeform/webby/blob/master/src/webby/multipart.nim
Thanks a lot for replying.. I went through your multipart implementation and it helped me understand what is really going to be sent to openai after the multipart/form-encoded request has been pieced together from procedure calls like multipart.addfiles, multipart[key] = val etc.
From what I understand every multipart entry is seperated from the others by a boundary(which often looks like ------random boundary ----), which must be kept constant throughout for a single request.
I echoed out the final multipart being sent and this is what I saw:
------------------------------ 0 ------------------------------
name="image"; filename="pic.png"
Content-Type: image/png
pic.png
------------------------------ 1 ------------------------------
name="mask"; filename="pic.png"
Content-Type: image/png
pic.png
------------------------------ 2 ------------------------------
name="prompt"
"A Nice Tesla For Asiwaju"
------------------------------ 3 ------------------------------
name="n"
2
------------------------------ 4 ------------------------------
name="size"
"512x512"
I understand that the output from this my echo is not exact depiction of what open ai receives but is just a high-level overview/summary of the whole thing. So I echoed the multipart repr and got this:
ref 0x7f42a199ed10 --> [content = 0x7f42a1e93230@[[name = 0x7f42a19f3510"image",
content = 0x7f42a19f3540"pic.png",
truefilename = 0x7f42a19f3570"pic.png",
contentType = 0x7f42a19f35a0"image/png",
fileSize = 0,
isStream = true], [name = 0x7f42a1947a50"mask",
content = 0x7f42a1947a80"pic.png",
truefilename = 0x7f42a1947ab0"pic.png",
contentType = 0x7f42a1947ae0"image/png",
fileSize = 0,
isStream = true], [name = 0x7f42a1947ba0"prompt",
content = 0x7f42a19486e0"\"A Nice Tesla For Asiwaju\"",
false], [name = 0x7f42a1947c60"n",
content = 0x7f42a1947c90"2",
false], [name = 0x7f42a1947d80"size",
content = 0x7f42a1947db0"\"512x512\"",
false]]]
which I feel is closer to what actually got sent to the server, except that it didnt include the binary file contents for pic.png.
And if you look closely you will see that my "512x512" is now being presented as "\"512x512\"", which is why openai is complaining.
I think you are creating a single entry for the entire json line. But each field in the json needs its > own part entry.
from what I have seen/echoed so far, it doesnt seem like so...
OpenAI api wants everything int its part.
please am sorry, I dont quite understand what you mean. The only place they wanted int was for the n parameter, which I provided as int, the rest where strings.
I think this is a bug in your create multi part form
I agree with you. This is because running :
let baz = Json {"image": "pic.png", "mask": "pic.png", "prompt": "A Nice Tesla For Asiwaju", "n": 2,
"size": "512x512"}
echo baz["size"].repr
gives :
ref 0x7f1882d9c750 --> [isUnquoted = false,
JStringstr = 0x7f1882d9aab0"512x512"]
which shows that my "512x512" is not turned to "\"512x512\"" under normal circumstances.
Which means the actual bug is somewhere in my code. To find it is the problem :(
I think you want some thing like this:
import puppy, webby/multipart
let req = newRequest("https://api.openai.com/v1/images/edits")
req.verb = "POST"
req.headers["Authorization"] = "Bearer " & readFile("OPENAI_API_KEY.txt")
var parts: seq[MultipartEntry]
parts.add(MultipartEntry(
name: "image",
fileName: "otter.png",
contentType: "image/png",
payload: readFile("otter.png")
))
parts.add(MultipartEntry(
name: "image",
fileName: "mask.png",
contentType: "image/png",
payload: readFile("mask.png")
))
parts.add(MultipartEntry(
name: "prompt",
payload: "A cute baby sea otter wearing a beret"
))
parts.add(MultipartEntry(
name: "n",
payload: "2"
))
parts.add(MultipartEntry(
name: "size",
payload: "1024x1024"
))
let (contentType, body) = encodeMultipart(parts)
req.headers["content-type"] = contentType
req.body = body
let res = req.fetch()
echo res.code
echo res.body