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
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