Skip to content

fix(chat-completions): accept object content parts and absent content field from WorkBuddy - #14

Open
Windyztree wants to merge 2 commits into
Jer-y:mainfrom
Windyztree:main
Open

fix(chat-completions): accept object content parts and absent content field from WorkBuddy#14
Windyztree wants to merge 2 commits into
Jer-y:mainfrom
Windyztree:main

Conversation

@Windyztree

Copy link
Copy Markdown

Problem

WorkBuddy's custom models (e.g. when proxying claude-opus-5) serialize a
message's content into shapes that fall outside the OpenAI spec, which only
allows content to be a string, an array of parts, or null. The proxy's
Zod validation then rejects the request:

Request validation failed: messages.N.content: Invalid input

Two distinct shapes trigger this:

  1. Single content-part objectcontent: { type: 'text', text: '...' }
    instead of the spec-required string / array.
  2. Absent content field — an assistant turn that carries tool_calls
    but omits content entirely (undefined). OpenAI treats this as null.

Both appear in real multi-turn agentic tool-call conversations, and the error
only surfaces once the conversation accumulates enough messages that the
offending shape reaches the validator.

Fix

  • Extend ChatCompletionsPayloadSchema to also accept a single content-part
    object and an absent/undefined content field.
  • Normalize in normalizeChatCompletionContent before forwarding (structure
    only, semantics unchanged):
    • { type: 'text', text } → plain string text
    • any other single object → one-element array [object]
    • absent contentnull

Changes

  • src/lib/schemas.tscontent union adds z.undefined() and .optional()
  • src/lib/chat-completions-content.ts — normalize absent content to null
  • src/routes/chat-completions/handler.ts — call normalizer after validation
  • tests/chat-completions-content-normalization.test.ts — unit + e2e for both
    shapes (9 tests, all passing)

Test plan

  • bun test tests/chat-completions-content-normalization.test.ts — 9 pass
  • Live check against a real claude-opus-5 multi-turn tool-call session:
    Invalid input no longer occurs; completions return 200.

Key diff

src/lib/schemas.ts

+    // WorkBuddy custom models also emit assistant messages with `tool_calls`
+    // but NO `content` field. OpenAI treats that as null, so accept it.
     content: z.union([
       z.string(),
       z.array(z.unknown()),
       z.record(z.string(), z.unknown()),
       z.null(),
+      z.undefined(),
-    ]),
+    ]).optional(),

src/lib/chat-completions-content.ts

 function normalizeMessageContent(message: Message): Message {
   const content = message.content as unknown

+  // WorkBuddy: assistant with tool_calls but no content -> treat as null
+  if (content === undefined)
+    return { ...message, content: null }
+
   if (content === null || typeof content === 'string' || Array.isArray(content))
     return message

…/array

Some OpenAI-compatible clients (e.g. WorkBuddy custom models) serialize a
message's `content` as a single content-part object (`{ type: "text", text:
"..." }`) instead of the spec-required string or array of parts. The proxy's
Zod schema only allowed string | array | null, so such requests failed with:

  Request validation failed: messages.N.content: Invalid input

- Extend ChatCompletionsPayloadSchema to also accept a single content-part object.
- Normalize in the chat-completions handler before forwarding: a {type:"text"}
  object becomes the plain string; any other object is wrapped in a one-element
  array. Strings, arrays, and null are unchanged, so upstream semantics are
  preserved and the Copilot /chat/completions endpoint receives a valid payload.

Adds unit + e2e tests covering the object-content shape.
…no content field

WorkBuddy custom models emit assistant turns that carry tool_calls but omit the
content field entirely. The chat-completions schema only allowed content as
string | array | object | null, so absent content failed validation with
'messages.N.content: Invalid input'. OpenAI treats an absent content as null,
so accept undefined/optional content and normalize it to null before forwarding.
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.

1 participant