Skip to content

Instantly share code, notes, and snippets.

@jozkee
Created April 3, 2026 05:44
Show Gist options
  • Select an option

  • Save jozkee/4c3ab76200a072f6b76788d8daa01f05 to your computer and use it in GitHub Desktop.

Select an option

Save jozkee/4c3ab76200a072f6b76788d8daa01f05 to your computer and use it in GitHub Desktop.

Proposal 4.1: CallId — Simplified Analysis & Options

Summary

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

Proposed Change 1: Keep CallId required, document synthetic id convention (Recommended)

// 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 id used as call_id) silently lose semantic distinction
  • Con: Gemini FunctionCall.id is 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

Proposed Change 2: Make CallId nullable on base types

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 processes FunctionCallContent which keeps required CallId
  • Gemini adapter: stops fabricating GUIDs for server tools
  • Foundry adapter: orphan types no longer need fabricated ids
  • Source-breaking: stringstring? for consumers without null checks

Proposed Change 3: Nullable constructor, non-null property (MEAI auto-generates)

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 id

Why 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 — CallId stays string
  • Pro: Adapters become simpler — no more GUID fabrication in each adapter
  • Pro: Single consistent synthetic id format across all adapters
  • Pro: Semantic clarity — null in constructor means "wire has no id"
  • Con: Constructor signature change — string to string? — 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 do new ToolResultContent(call.CallId). Same discipline required as 2a, but auto-generation makes the mistake silent. Mitigation: a factory like ToolResultContent.ForCall(callContent) could enforce pairing, but adds API surface for a niche problem.

Proposed Change 4: Remove CallId from base types

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 ToolCallContent to read CallId must cast to specific subtypes instead
  • Con: Third-party adapters creating ToolCallContent with a CallId need to subclass or use AdditionalProperties
  • Con: Generic middleware that correlates call↔result by CallId on the base type would break
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment