ToolCallContent.CallId and ToolResultContent.CallId are currently required non-null
(Throw.IfNull). Not all provider wire types have a natural correlation identifier.
Of 64 tool call/output wire types across 4 providers, 15 lack a natural non-null CallId:
| Provider | Wire Type | Role | Family | Notes |
|---|---|---|---|---|
| Azure Foundry | MemorySearchToolCallItemParam |
call | memory-search | No id on wire at all |
| Azure Foundry | MemorySearchToolCallItemResource |
call+output | memory-search | No id on wire at all |
| Azure Foundry | WorkflowActionOutputItem |
output | workflow-action | No id on wire at all |
| Gemini | ExecutableCode |
call | code-execution | id optional per spec; Gemini SDK ignores it, fabricates GUID |
| Gemini | CodeExecutionResult |
output | code-execution | id optional per spec; Gemini SDK ignores it, fabricates GUID |
| Gemini | ToolCall |
call | generic-server-tool | id optional per spec; Gemini SDK ignores it, fabricates GUID |
| Gemini | ToolResponse |
output | generic-server-tool | id optional per spec; Gemini SDK ignores it, fabricates GUID |
| Gemini | FunctionCall |
call | function | id optional per spec; SDK uses id ?? "" |
| Gemini | FunctionResponse |
output | function | id optional per spec; SDK uses id ?? "" |
| Azure Foundry | OAuthConsentRequestOutputItem |
output | oauth-consent | Has id but no call_id; adapter uses id as CallId |
| OpenAI | CodeInterpreterToolCall |
call+output | code-interpreter | Hybrid: output embedded in call. Has id but no call_id; adapter uses id |
| OpenAI | FileSearchToolCall |
call+output | file-search | Hybrid: output embedded in call. Has id but no call_id; adapter uses id |
| OpenAI | ImageGenToolCall |
call+output | image-gen | Hybrid: output embedded in call. Has id but no call_id; adapter uses id |
| OpenAI | MCPToolCall |
call+output | mcp | Hybrid: output embedded in call. Has id but no call_id; adapter uses id |
| OpenAI | WebSearchToolCall |
call | web-search | No separate output type. Has id but no call_id; adapter uses id |
The remaining 49 types have a natural non-null correlation id:
| Provider | Wire Type | Role | Family | Notes |
|---|---|---|---|---|
| Anthropic | ResponseToolUseBlock |
call | function | id — outputs reference via tool_use_id |
| Anthropic | ResponseMCPToolUseBlock |
call | mcp | id — outputs reference via tool_use_id |
| Anthropic | ResponseServerToolUseBlock |
call | server-tool | id — outputs reference via tool_use_id |
| Anthropic | RequestToolResultBlock |
output | function | tool_use_id references call's id |
| Anthropic | ResponseMCPToolResultBlock |
output | mcp | tool_use_id references call's id |
| Anthropic | ResponseBashCodeExecutionToolResultBlock |
output | bash-code-execution | tool_use_id references call's id |
| Anthropic | ResponseCodeExecutionToolResultBlock |
output | code-execution | tool_use_id references call's id |
| Anthropic | ResponseTextEditorCodeExecutionToolResultBlock |
output | text-editor | tool_use_id references call's id |
| Anthropic | ResponseToolSearchToolResultBlock |
output | tool-search | tool_use_id references call's id |
| Anthropic | ResponseWebFetchToolResultBlock |
output | web-fetch | tool_use_id references call's id |
| Anthropic | ResponseWebSearchToolResultBlock |
output | web-search | tool_use_id references call's id |
| Azure Foundry | A2AToolCall |
call | a2a | call_id |
| Azure Foundry | A2AToolCallOutput |
output | a2a | call_id |
| Azure Foundry | AzureAISearchToolCall |
call | azure-ai-search | call_id |
| Azure Foundry | AzureAISearchToolCallOutput |
output | azure-ai-search | call_id |
| Azure Foundry | AzureFunctionToolCall |
call | azure-function | call_id |
| Azure Foundry | AzureFunctionToolCallOutput |
output | azure-function | call_id |
| Azure Foundry | BingCustomSearchToolCall |
call | bing-custom-search | call_id |
| Azure Foundry | BingCustomSearchToolCallOutput |
output | bing-custom-search | call_id |
| Azure Foundry | BingGroundingToolCall |
call | bing-grounding | call_id |
| Azure Foundry | BingGroundingToolCallOutput |
output | bing-grounding | call_id |
| Azure Foundry | BrowserAutomationToolCall |
call | browser-automation | call_id |
| Azure Foundry | BrowserAutomationToolCallOutput |
output | browser-automation | call_id |
| Azure Foundry | FabricDataAgentToolCall |
call | fabric-data-agent | call_id |
| Azure Foundry | FabricDataAgentToolCallOutput |
output | fabric-data-agent | call_id |
| Azure Foundry | OpenApiToolCall |
call | openapi | call_id |
| Azure Foundry | OpenApiToolCallOutput |
output | openapi | call_id |
| Azure Foundry | SharepointGroundingToolCall |
call | sharepoint-grounding | call_id |
| Azure Foundry | SharepointGroundingToolCallOutput |
output | sharepoint-grounding | call_id |
| OpenAI | FunctionToolCall |
call | function | call_id |
| OpenAI | FunctionToolCallOutput |
output | function | call_id |
| OpenAI | ComputerToolCall |
call | computer-use | call_id |
| OpenAI | ComputerToolCallOutput |
output | computer-use | call_id |
| OpenAI | CustomToolCall |
call | custom | call_id |
| OpenAI | CustomToolCallOutput |
output | custom | call_id |
| OpenAI | ApplyPatchToolCall |
call | apply-patch | call_id |
| OpenAI | ApplyPatchToolCallOutput |
output | apply-patch | call_id |
| OpenAI | FunctionShellCall |
call | function-shell | call_id |
| OpenAI | FunctionShellCallOutput |
output | function-shell | call_id |
| OpenAI | LocalShellToolCall |
call | local-shell | call_id |
| OpenAI | LocalShellToolCallOutput |
output | local-shell | call_id — missing from spec properties but in required (openai/openai-openapi#532) |
| OpenAI | ToolSearchCall |
call | tool-search | call_id |
| OpenAI | ToolSearchOutput |
output | tool-search | call_id |
Resource/ItemParam variants (e.g., FunctionToolCallOutputResource, ApplyPatchToolCallOutputItemParam)
are the same shape at different API boundaries — omitted for simplicity.
Total: 55 deduplicated types = 42 natural + 3 fabricated + 6 synthetic (includes 2 nullable) + 6 conflated. (64 including Resource/ItemParam variants.)
// No API change — CallId stays string (non-null)
// Each adapter fabricates its own synthetic ids
class ToolCallContent : AIContent
{
string CallId { get; } // unchanged
}Convention: Adapters MUST provide a CallId value for every ToolCallContent/ToolResultContent.
When the wire format has no correlation id, adapters SHOULD fabricate a stable synthetic id
(e.g., Guid.NewGuid().ToString("N")) and ensure call/result pairs share the same value.
Rationale: Keeps the API simple — consumers never need null checks. The 15 problematic types are a small minority (23%), and adapters already fabricate ids today.
Trade-offs:
- Pro: No breaking change, simpler consumer code
- Pro: CallId always available for logging, telemetry, matching
- Con: Synthetic ids are meaningless — they don't survive round-trips, can't be used for debugging
- Con: Conflated ids (OpenAI
idused ascall_id) silently lose semantic distinction - Con: Gemini
FunctionCall.idis nullable per spec —?? ""fallback masks data absence - Con: Enshrines current adapter workarounds as the intended design
- Con: Every adapter must independently implement the same GUID-generation pattern
class ToolCallContent : AIContent
{
string? CallId { get; } // was: string with Throw.IfNull
}
class ToolResultContent : AIContent
{
string? CallId { get; } // was: string with Throw.IfNull
}
// Function subtypes keep CallId effectively required
class FunctionCallContent : ToolCallContent { /* constructor still requires callId */ }
class FunctionResultContent : ToolResultContent { /* constructor still requires callId */ }Rationale: 15 types have no natural non-null correlation id. Adapters currently fabricate
GUIDs or repurpose unrelated fields. Making CallId nullable lets adapters honestly represent
what the wire provides.
Impact:
FunctionInvokingChatClient: no change — only processesFunctionCallContentwhich keeps requiredCallId- Gemini adapter: stops fabricating GUIDs for server tools
- Foundry adapter: orphan types no longer need fabricated ids
- Source-breaking:
string→string?for consumers without null checks
class ToolCallContent : AIContent
{
public ToolCallContent(string? callId = null)
{
CallId = callId ?? Guid.NewGuid().ToString("N");
}
public string CallId { get; } // stays non-null for consumers
}
class ToolResultContent : AIContent
{
public ToolResultContent(string? callId = null)
{
CallId = callId ?? Guid.NewGuid().ToString("N");
}
public string CallId { get; } // stays non-null for consumers
}
// Function subtypes unchanged — constructor still requires callId
class FunctionCallContent : ToolCallContent { }
class FunctionResultContent : ToolResultContent { }How adapters use it:
// Natural id — pass it through
var call = new ToolCallContent("call_xyz789");
// No id on wire — pass null, MEAI generates synthetic id
var call = new ToolCallContent(callId: null);
var result = new ToolResultContent(call.CallId); // reuses generated idWhy call/result pairing works for all 15 problem types:
| Category | Types | Why pairing works |
|---|---|---|
| Fabricated (3) | Foundry MemorySearch, WorkflowAction | Hybrid/single-response — adapter creates both in same code path |
| Synthetic (4) | Gemini code-exec, generic-server-tool | Both arrive in same model response — adapter creates both together |
| Nullable (2) | Gemini FunctionCall/Response | FunctionInvokingChatClient already copies callContent.CallId to result |
| Conflated (6) | OpenAI hybrids, Foundry OAuth | Call+result created in same switch case |
Rationale: Centralizes synthetic id generation in MEAI instead of each adapter independently.
Adapters express intent ("I don't have a call id") instead of hiding it behind fabricated values.
Consumer code is unchanged — CallId is always non-null.
Trade-offs:
- Pro: No breaking change for consumers —
CallIdstaysstring - Pro: Adapters become simpler — no more GUID fabrication in each adapter
- Pro: Single consistent synthetic id format across all adapters
- Pro: Semantic clarity —
nullin constructor means "wire has no id" - Con: Constructor signature change —
stringtostring?— adapters need minor update - Con: Synthetic ids still don't survive wire round-trips
- Con: Cannot distinguish "adapter had no id" from "adapter chose to auto-generate" after construction
- Con: Pairing footgun — nothing prevents
new ToolResultContent(callId: null)generating a different id than its call. Adapter must know to donew ToolResultContent(call.CallId). Same discipline required as 2a, but auto-generation makes the mistake silent. Mitigation: a factory likeToolResultContent.ForCall(callContent)could enforce pairing, but adds API surface for a niche problem.
class ToolCallContent : AIContent
{
// No CallId — base type makes no correlation promise
string? Name { get; set; }
}
class ToolResultContent : AIContent
{
// No CallId — base type makes no correlation promise
}
// Only subtypes that need wire round-trip have CallId
class FunctionCallContent : ToolCallContent
{
public FunctionCallContent(string callId, string name, ...) { ... }
public string CallId { get; } // required — always comes from wire
}
class FunctionResultContent : ToolResultContent
{
public FunctionResultContent(string callId, ...) { ... }
public string CallId { get; } // required — always goes back on wire
}
class McpServerToolCallContent : ToolCallContent
{
public string CallId { get; } // needed for internal dictionary correlation
}
class McpServerToolResultContent : ToolResultContent
{
public string CallId { get; } // paired with McpServerToolCallContent.CallId
}Rationale: The base type currently promises a correlation id that 15 of 64 wire types can't
deliver and 16 more (unmapped OpenAI types) don't even use — they fall through to generic
AIContent. Only FunctionCallContent and McpServerToolCallContent actually rely on CallId
for wire round-trip or internal correlation. Moving CallId to the subtypes that need it
eliminates fabrication, nullability, and conflation entirely.
Trade-offs:
- Pro: Cleanest semantically — base type doesn't promise what it can't deliver
- Pro: No fabrication, no nullability, no conflation
- Pro: Subtypes that need CallId still have it, required and non-null
- Con: Biggest breaking change — consumers casting to
ToolCallContentto readCallIdmust cast to specific subtypes instead - Con: Third-party adapters creating
ToolCallContentwith a CallId need to subclass or useAdditionalProperties - Con: Generic middleware that correlates call↔result by
CallIdon the base type would break