Skip to content

Per-provider tool schema in processToolCallingRequest() #20

@nito-web

Description

@nito-web

Hi there,

Provider-specific tool schemas are the old cross-SDK headache — anyone who's wired the same logic across OpenAI/Anthropic/Gemini has hit it (in my case in Python land in the time before MCP). Opening this because the way it lands in AiM is fixable in one place, and there's already a PR underway that gets us partway.

ToolDefinition::toArray() emits one shape; the bridges we install actually need four:

a) OpenAI Chat Completions — nested under function: {type:"function", function:{name, …}}. This is what toArray() emits today.
b) OpenAI Responses API (/v1/responses) — flat: {type:"function", name, …} at the top level. Both ai-open-ai-platform and ai-open-responses-platform POST here, so the nested shape 400s with "Missing required parameter: 'tools[0].name'."
c) Anthropic Messages — {name, description, input_schema}, no wrapper.
d) Gemini — tools: [{functionDeclarations: [{name, …}]}], with the declarations collapsed into a single tools[] entry.

Ollama is its own situation but doesn't add a fifth dialect — it's a runtime exposing four endpoints that each speak one of (a)/(b)/(c). The symfony-ai Ollama bridge POSTs to /api/chat, which is (a), so it's already happy with the current default.

Everything funnels through one spot: SymfonyAiPlatformAdapter::processToolCallingRequest() line ~190 (the array_map(… $tool->toArray() …) line). Nice precedent right next to it: resolveMaxTokensKey() already switches per bridge for the max_tokens option key — same pattern would fit cleanly here.

I saw #8 by @dkd-dobberkau adds the Anthropic arm — that's great. The else branch still emits the nested (a) shape though, so OpenAI users hitting /v1/responses stay broken; and the strict flag gets dropped along the way.

I've patched it locally to unblock Claude Sonnet on my project and have a plan written up covering the missing OpenAI Responses + Gemini arms with a verification step per provider. Happy to contribute it back.

Before I start — do you prefer:

a PR against the existing PR #8 branch (extending it), or waiting for #8 to merge and opening a follow-up against main?
There are a few ways to structure the code (inline match, static helper mirroring resolveMaxTokensKey(), or a per-provider serializer interface) — happy to DM on Slack if that's easier than going back and forth here.

Cheers! Nik

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