Skip to content

CF provider: malformed messages for multi-turn tool conversations via /ai/v1/chat/completions #91

@stackbilt-admin

Description

@stackbilt-admin

Summary

When AI_GATEWAY_ID is set (enabling the /ai/v1/chat/completions path in cloudflare-ai-binding.ts), the Cloudflare provider fails on multi-turn tool conversations with:

```
AiError: Bad input: Error: oneOf at '/' not met, 0 matches:
required properties at '/' are 'prompt',
Type mismatch of '/messages/0/content', 'array' not in 'string',
Type mismatch of '/messages/1/content', 'string' not in 'null',
required properties at '/messages/1' are 'role,content'
```

Single-turn tool requests succeed. The bug only surfaces when messages contains prior function_call + function_call_output history.

Root cause (localized)

cloudflare.tsformatRequest() message builder (around line 588–623):

When the request includes messages with toolCalls and toolResults, the resulting messages array contains:

  • A user message whose content ends up as an array (not a string) in some paths — CF's /ai/v1/chat/completions requires string content
  • An assistant message with content: null and tool_calls — the endpoint schema may require this to be absent rather than null

The /ai/run/{model} path (used without AI_GATEWAY_ID) does not exhibit this error — its schema appears more permissive.

Impact

  • Any multi-turn tool-loop request routed through cloudflare provider via AI_GATEWAY_ID fails
  • Affects @cf/openai/gpt-oss-120b, @cf/moonshotai/kimi-k2.6, and all other CF tool-capable models
  • In llm-gateway: the error is a plain Error (not a typed LLMProviderError), which additionally blocks fallback (separate gateway-side workaround already applied)

Suggested fix

In cloudflare.ts formatRequest():

  1. Ensure all message.content values are coerced to string | null, never string[], before building the CF payload
  2. For assistant messages with tool_calls, verify whether the /ai/v1/chat/completions endpoint requires content to be omitted vs null
  3. Consider throwing InvalidRequestError (or another typed error) instead of propagating the raw AiError, so callers can distinguish bad-input from transient failures

Reproduction

// Construct an LLMRequest with prior tool loop history:
const request = {
  messages: [
    { role: 'user', content: 'list files' },
    { role: 'assistant', content: '', toolCalls: [{ id: 'c1', type: 'function', function: { name: 'bash', arguments: '{"command":"ls"}' } }] },
    { role: 'user', content: 'file1\nfile2', toolResults: [{ id: 'c1', output: 'file1\nfile2' }] },
  ],
  tools: [{ type: 'function', function: { name: 'bash', description: 'run shell', parameters: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] } } }],
  model: '@cf/openai/gpt-oss-120b',
};
// CloudflareProvider.generateResponse(request) → AiError: Bad input

Environment

  • @stackbilt/llm-providers v1.14.4
  • AI_GATEWAY_ID set (triggers /ai/v1/chat/completions path in binding)
  • Verified: /ai/run/ endpoint works for same payload, /ai/v1/chat/completions fails

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions