Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BugFix][Refactor] Modular Transformer Pipeline and Fix Gemini/Anthropic Empty Content Handling #6063

Open
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

SongChiYoung
Copy link
Contributor

@SongChiYoung SongChiYoung commented Mar 22, 2025

Why are these changes needed?

This change addresses a compatibility issue when using Google Gemini models with AutoGen. Specifically, Gemini returns a 400 INVALID_ARGUMENT error when receiving a response with an empty "text" parameter.

The root cause is that Gemini does not accept empty string values (e.g., "") as valid inputs in the history of the conversation.

To fix this, if the content field is falsy (e.g., None, "", etc.), it is explicitly replaced with a single whitespace (" "), which prevents the Gemini model from rejecting the request.

  • Gemini API compatibility: Gemini models reject empty assistant messages (e.g., ""), causing runtime errors. This PR ensures such messages are safely replaced with whitespace where appropriate.
  • Avoiding regressions: Applying the empty content workaround only to Gemini, and only to valid message types, avoids breaking OpenAI or other models.
  • Reducing duplication: Previously, message transformation logic was scattered and repeated across different message types and models. Modularizing this pipeline removes that redundancy.
  • Improved maintainability: With future model variants likely to introduce more constraints, this modular structure makes it easier to adapt transformations without writing ad-hoc code each time.
  • Testing for correctness: The new structure is verified with tests, ensuring the bug fix is effective and non-intrusive.

Summary

This PR introduces a modular transformer pipeline for message conversion and fixes a Gemini-specific bug related to empty assistant message content.

Key Changes

  • [Refactor] Extracted message transformation logic into a unified pipeline to:

    • Reduce code duplication
    • Improve maintainability
    • Simplify debugging and extension for future model-specific logic
  • [BugFix] Gemini models do not accept empty assistant message content.

    • Introduced _set_empty_to_whitespace transformer to replace empty strings with " " only where needed
    • Applied it only to "text" and "thought" message types, not to "tools" to avoid serialization errors
  • Improved structure for model-specific handling

    • Transformer functions are now grouped and conditionally applied based on message type and model family
    • This design makes it easier to support future models or combinations (e.g., Gemini + R1)
  • Test coverage added

    • Added dedicated tests to verify that empty assistant content causes errors for Gemini
    • Ensured the fix resolves the issue without affecting OpenAI models

Motivation

Originally, Gemini-compatible endpoints would fail when receiving assistant messages with empty content ("").
This issue required special handling without introducing brittle, ad-hoc patches.

In addressing this, I also saw an opportunity to modularize the message transformation logic across models.
This improves clarity, avoids duplication, and simplifies future adaptations (e.g., different constraints across model families).


📘 AutoGen Modular Message Transformer: Design & Usage Guide

This document introduces the new modular transformer system used in AutoGen for converting LLMMessage instances to SDK-specific message formats (e.g., OpenAI-style ChatCompletionMessageParam).
The design improves reusability, extensibility, and maintainability across different model families.


🚀 Overview

Instead of scattering model-specific message conversion logic across the codebase, the new design introduces:

  • Modular transformer functions for each message type
  • Per-model transformer maps (e.g., for OpenAI-compatible models)
  • Optional conditional transformers for multimodal/text hybrid models
  • Clear separation between message adaptation logic and SDK-specific builder (e.g., ChatCompletionUserMessageParam)

🧱 1. Define Transform Functions

Each transformer function takes:

  • LLMMessage: a structured AutoGen message
  • context: dict: metadata passed through the builder pipeline

And returns:

  • A dictionary of keyword arguments for the target message constructor (e.g., {"content": ..., "name": ..., "role": ...})
def _set_thought_as_content_gemini(message: LLMMessage, context: Dict[str, Any]) -> Dict[str, str | None]:
    assert isinstance(message, AssistantMessage)
    return {"content": message.thought or " "}

🪢 2. Compose Transformer Pipelines

Multiple transformer functions are composed into a pipeline using build_transformer_func():

base_user_transformer_funcs: List[Callable[[LLMMessage, Dict[str, Any]], Dict[str, Any]]] = [
    _assert_valid_name,
    _set_name,
    _set_role("user"),
]

user_transformer = build_transformer_func(
    funcs=base_user_transformer_funcs,
    message_param_func=ChatCompletionUserMessageParam
)
  • The message_param_func is the actual constructor for the target message class (usually from the SDK).
  • The pipeline is ordered — each function adds or overrides keys in the builder kwargs.

🗂️ 3. Register Transformer Map

Each model family maintains a TransformerMap, which maps LLMMessage types to transformers:

__BASE_TRANSFORMER_MAP: TransformerMap = {
    SystemMessage: system_transformer,
    UserMessage: user_transformer,
    AssistantMessage: assistant_transformer,
}

register_transformer("openai", model_name_or_family, __BASE_TRANSFORMER_MAP)
  • "openai" is currently required (as only OpenAI-compatible format is supported now).
  • Registration ensures AutoGen knows how to transform each message type for that model.

🔁 4. Conditional Transformers (Optional)

When message construction depends on runtime conditions (e.g., "text" vs. "multimodal"), use:

conditional_transformer = build_conditional_transformer_func(
    funcs_map=user_transformer_funcs_claude,
    message_param_func_map=user_transformer_constructors,
    condition_func=user_condition,
)

Where:

  • funcs_map: maps condition label → list of transformer functions
user_transformer_funcs_claude = {
    "text": text_transformers + [_set_empty_to_whitespace],
    "multimodal": multimodal_transformers + [_set_empty_to_whitespace],
}
  • message_param_func_map: maps condition label → message builder
user_transformer_constructors = {
    "text": ChatCompletionUserMessageParam,
    "multimodal": ChatCompletionUserMessageParam,
}
  • condition_func: determines which transformer to apply at runtime
def user_condition(message: LLMMessage, context: Dict[str, Any]) -> str:
    if isinstance(message.content, str):
        return "text"
    return "multimodal"

🧪 Example Flow

llm_message = AssistantMessage(name="a", thought="let’s go")
model_family = "openai"
model_name = "claude-3-opus"

transformer = get_transformer(model_family, model_name, type(llm_message))
sdk_message = transformer(llm_message, context={})

🎯 Design Benefits

Feature Benefit
🧱 Function-based modular design Easy to compose and test
🧩 Per-model registry Clean separation across model families
⚖️ Conditional support Allows multimodal / dynamic adaptation
🔄 Reuse-friendly Shared logic (e.g., _set_name) is DRY
📦 SDK-specific Keeps message adaptation aligned to builder interface

🔮 Future Direction

  • Support more SDKs and formats by introducing new message_param_func
  • Global registry integration (currently "openai"-scoped)
  • Class-based transformer variant if complexity grows

Related issue number

Closes #5762

Checks

@SongChiYoung
Copy link
Contributor Author

While preparing this fix, I noticed we currently don't have explicit tests validating empty-string handling for Gemini API.

Would it be helpful if I added a dedicated test case directly in this PR? Or should we handle the test separately in a follow-up PR/issue?

Please let me know what you prefer—happy to help either way!

@ekzhu
Copy link
Collaborator

ekzhu commented Mar 22, 2025

Yes. It would be good to include the test as part of this PR.

Also, we can use model family to check if we need to make the white space adjustment, rather than doing this for all model API

@ekzhu
Copy link
Collaborator

ekzhu commented Mar 22, 2025

Another thing is instead of modifying the returned content, we should be modifying the messages that got sent to the model api. So we need to modify to_oai_types function and enable option to use white space instead of empty string for empty content, if the model family is Gemini

@SongChiYoung
Copy link
Contributor Author

SongChiYoung commented Mar 22, 2025

@ekzhu
After thoroughly considering your feedback, I've become convinced that handling the empty-string issue before sending messages (at the request phase) indeed offers better robustness, maintainability, and extensibility—especially if similar compatibility issues arise with other models in the future.

Therefore, I'll implement a closure-based approach that:

  • Defines a message-transformer function once at the API call preparation step.
  • Passes this transformer down through the message-processing functions.
  • Avoids repeated conditional checks, ensuring minimal overhead.

However, I still see some merit in the original approach (modifying responses), as it keeps changes minimal and simple. If you prefer simplicity and minimal impact on existing code over the extensibility provided by the closure approach, I'd be happy to revert to the original implementation.

I'll proceed with the closure-based solution for now, but please let me know if you'd prefer the simpler response-handling option instead. Happy to accommodate your preference!

here is sample code for closure case

# Define closure once at the top level
def create_content_transformer(model_family: ModelFamily):
    if model_family == ModelFamily.GEMINI:
        return lambda c: " " if not c else c
    else:
        return lambda c: c

# Usage at API request call-site
content_transformer = create_content_transformer(model_family)
oai_messages = [
    to_oai_type(msg, content_transformer=content_transformer)
    for msg in messages
]

# Usage at token counting
token_count_messages = [
    to_oai_type(msg, content_transformer=content_transformer)
    for msg in token_count_messages
]

# at to_oai_type
def to_oai_type(
    message: LLMMessage,
    prepend_name: bool = False,
    content_transformer: Callable[[str], str] = lambda x: x
) -> Sequence[ChatCompletionMessageParam]:

    if isinstance(message, SystemMessage):
        return [system_message_to_oai(message, content_transformer)]
    elif isinstance(message, UserMessage):
        return [user_message_to_oai(message, prepend_name, content_transformer)]
    elif isinstance(message, AssistantMessage):
        return [assistant_message_to_oai(message, content_transformer)]
    else:
        return tool_message_to_oai(message, content_transformer)

@ekzhu
Copy link
Collaborator

ekzhu commented Mar 23, 2025

@SongChiYoung I think what you are doing is a useful step toward a more modular way to manage different message transformation logic for each model family. I think your new approach is in the right direction.

I would consider making it more modular and easier to maintain by having a global dictionary of separate transformation functions that goes from a list of LLMMessage to ChatCompletionMessageParam for each model family.

So we can then refactor the model client to use this dictionary. This will make it easier to solve other problem like this one #6034 easier.

As a next step in a different PR, we can address the parsing of Choice content using similar approach. This allows us to address issues like R1 model response parsing #5961

@SongChiYoung
Copy link
Contributor Author

@ekzhu Thanks for your valuable feedback!
As you suggested, I'm planning to set up a global registry that maps each ModelFamily to a list of transformer functions, rather than a single transformer. A simple closure will combine these transformers into a single callable per ModelFamily.

Motivation:

  • Extensibility: Easily handle future model feature combinations (e.g., Gemini + R1 or other mixed requirements).
  • Maintainability: Clearly show each step of the transformation, making debugging and adding new transformers straightforward.

Additionally, I'll place this global registry within a dedicated transform package at the top level of autogen-ext. This cleanly separates the transformer logic from the existing codebase, enhancing discoverability and future maintainability.

Based on your previous comment, it seems like we're on the same page regarding this structural direction, but I just wanted to confirm once more before proceeding. Let me know if you agree or have additional suggestions—once confirmed, I'll try the implementation!

@ekzhu
Copy link
Collaborator

ekzhu commented Mar 23, 2025

Additionally, I'll place this global registry within a dedicated transform package at the top level of autogen-ext. This cleanly separates the transformer logic from the existing codebase, enhancing discoverability and future maintainability.

I would keep this within the autoen_ext.models.openai namespace for now. This is for addressing the compatibility issues with "OpenAI-compatible" endpoints

@SongChiYoung SongChiYoung force-pushed the FIX/Gemnini_empty_steream branch from b61a868 to 82c50ce Compare March 23, 2025 18:15
@SongChiYoung
Copy link
Contributor Author

SongChiYoung commented Mar 23, 2025

@ekzhu Thanks again for your thoughtful feedback!

Please check my new changes of code.

  • Global transformer registry by model family
  • Removed duplicated transformation logic by unifying into a shared registry
  • Gemini assistant fix for empty content / thought
  • Added tests for Gemini tool calling edge cases
Details
  • Introduced a global registry for message transformers mapped by ModelFamily
  • Unified previously duplicated transformer logic (e.g., assistant conditionals) into centralized registry
  • Applied Gemini-specific assistant transformers with fallback for empty content
  • Fixed Gemini tool-calling issue caused by empty content or thought
  • Added test cases to assert Gemini behavior on invalid assistant messages
I completely understand your point about keeping the registry within the `autogen_ext.models.openai` namespace for now, especially since the immediate goal is resolving compatibility issues with OpenAI-compatible endpoints.

Before finalizing the change, I just wanted to briefly share why I initially designed the global transformer registry at the top-level namespace (autogen-ext/transformation):

  • Extensibility: This structure easily allows future integrations with other model families beyond OpenAI-compatible APIs (e.g., Gemini, R1, and any new emerging models).
  • Maintainability: Clearly defined and separated transformer logic facilitates easier debugging, reviewing, and extending functionality in the long run.

I fully respect your initial suggestion and I'm happy to move forward by placing it under autogen_ext.models.openai as you recommended. But if there's still a possibility to reconsider placing it at the global level—even temporarily—I would greatly appreciate your thoughts once more.

Could you please let me know your final thoughts on this?

Thanks again for your time and patience!

@SongChiYoung SongChiYoung changed the title [BugFix] Handle empty content for Gemini model by replacing with whitespace [BugFix][Refactor] Modular Transformer Pipeline and Fix Gemini Empty Content Handling Mar 24, 2025
@SongChiYoung
Copy link
Contributor Author

Updated the PR title and description to better reflect the scope of changes. Let me know if anything else needs clarification!

@SongChiYoung
Copy link
Contributor Author

@ekzhu As I mentioned in the recent issue I filed (#6083), Anthropic models also raise errors when given empty content — similar to the Gemini case. This applies to both OpenAI-compatible and native Anthropic SDKs.

To address this properly, I realized that the transformer logic needs to apply more broadly — not just under the OpenAI namespace.

I’m truly sorry to repeat this suggestion again, as I know I previously proposed a similar idea. However, this additional context (supporting Anthropic and potentially other models) makes it clearer that placing the transformer registry under a top-level transform package within autogen_ext would help avoid duplicated logic and keep things clean moving forward.

Please let me know what you think — and I really appreciate your patience reviewing this again.

@ekzhu
Copy link
Collaborator

ekzhu commented Mar 25, 2025

They @SongChiYoung thanks for the update.

I understand that it is much more modular with the new approach with potential added benefit, but the PR is getting quite large, and it introduces a new architecture that maybe we as maintainers aren't extremely familiar with.

Consider this, you may stop contributing to the project after a few months with very good reason as you may start work on other things interest you. But we will still be left with the code we may not understand very well.

So, my suggestion is to make the changes incrementally and think about what the minimal design is needed to solve the problems related to OpenAIChatCompletionClient when used for different compatibility endpoints. I think the minimal design is simply a register dictionary with model family prefix and their respective to_oai_type functions. Just need to make sure the top-level to_oai_type function all have the same signature and we are good. This solves the urgent issue and leave a lot of room for future improvements.

Right now, I'd say beefing up the integration test cases for OpenAIChatCompletionClient is more important than creating a new architecture for message transformation. Right now, the tests only use OpenAI and Gemini models, but we hope to increase the modularity and expand to more providers.

@SongChiYoung
Copy link
Contributor Author

Thank you @ekzhu — I really appreciate your thoughtful feedback, and your team’s dedication to maintaining the quality and long-term sustainability of AutoGen.

Your suggestion makes perfect sense, and I’ll proceed as you advised:

  • I’ll simplify the current implementation to use a registry of ModelFamily -> to_oai_type functions.
  • I’ll also keep everything scoped within the OpenAI namespace for now, without introducing broader architectural changes.

In addition, since the Anthropic SDK is specific to Anthropic models only, I’ll go ahead and apply the simple fix there as well — without needing any model check logic — and include that in this PR.

Thanks again, and I’ll update the PR shortly!

@ekzhu
Copy link
Collaborator

ekzhu commented Mar 26, 2025

Thank you very much for your work!

@SongChiYoung
Copy link
Contributor Author

@ekzhu
I've now completed the remaining updates for this PR. Here's a quick summary of what's been addressed:

  1. ✅ As suggested, I moved the transformer registry and related logic from autogen-ext/transformation to autogen_ext/models/openai/. The registry now lives entirely within the OpenAI-compatible namespace, as you requested.

  2. ✅ I reviewed PR #5989, which introduces reasoning tokens in streaming events. Since the Thought message type already exists and is fully integrated in the message pipeline, this PR ([BugFix][Refactor] Modular Transformer Pipeline and Fix Gemini/Anthropic Empty Content Handling #6063) does not require any change to support it. I also verified that all tests pass with Add a thought process analysis, and add a reasoning field in the ModelClientStreamingChunkEvent to distinguish the thought tokens. #5989 merged.

  3. ✅ Regarding issue #6083: I applied a simple and safe fix for Anthropic models, which also reject empty assistant content. The fix ensures compatibility without relying on model checks, and I added dedicated test coverage for this case.

  4. ✅ At this point, I believe the PR is feature-complete. All tests are passing, and the changes are now strictly scoped and modular. Please let me know if there's anything else you'd like me to revise or improve—happy to adjust!

  5. 🛠️ As a follow-up, I plan to open a separate PR(FEAT: Add missing OpenAI-compatible models (GPT-4.5, Claude models) #6120) to:

    • Register Anthropic models (e.g., Claude 3) in _model_info.py
    • Add GPT-4.5 family to the model list for better test coverage and future support

Let me know what you think—appreciate your time and thoughtful guidance throughout this process!

@SongChiYoung SongChiYoung changed the title [BugFix][Refactor] Modular Transformer Pipeline and Fix Gemini Empty Content Handling [BugFix][Refactor] Modular Transformer Pipeline and Fix Gemini/Anthropic Empty Content Handling Mar 26, 2025
@@ -142,20 +143,33 @@ def get_mime_type_from_image(image: Image) -> Literal["image/jpeg", "image/png",
return "image/jpeg"


def __empty_content_to_whitespace(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using a single function with multiple return types, I think you can just split this into two functions for one return type each. That way you don't need to do cast which can be a source of bug as you bypassed type checking

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ekzhu Thanks for the suggestion! I agree that avoiding cast() is the safer and more maintainable approach.

Instead of splitting the logic into two separate functions, I’m planning to update the implementation using @overload to preserve a unified structure while keeping it type-safe.

I'll adjust the return and argument types accordingly — likely using something like str | Iterable[Any] — in a way that works cleanly with the rest of the codebase.

Appreciate the feedback, and I’ll follow up with the revision soon!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ekzhu Changed. Plz check it.

return {"content": prefix + message.content}


def _set_multimodal_content(message: LLMMessage, context: Dict[str, Any]) -> Dict[str, Any]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since now we are exclusively dealing with OpenAI types we should be using the types from OpenAI library. This helps us to make sure things are validated via type checking and avoid the use of cast.



# === Base Transformers list ===
base_system_message_transformers: List[Callable[[LLMMessage, Dict[str, Any]], Dict[str, Any]]] = [
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here and everywhere else: use types from the OpenAI library to ensure we are covered by type checking.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored to avoid cast() by enforcing TransformerFunc to return Sequence[MessageParam].
This preserves type safety while keeping the registry extensible beyond OpenAI.

FunctionExecutionResultMessage: function_execution_result_message,
}

__CLAUDE_TRANSFORMER_MAP: TransformerMap = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought Claude isn't OpenAI compatible?

Copy link
Contributor Author

@SongChiYoung SongChiYoung Mar 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now claude has openai API : https://docs.anthropic.com/en/api/openai-sdk
And current use it.(without PR #6120)

anthropic_openai_client = lambda: OpenAIChatCompletionClient(
    model="claude-3-7-sonnet-20250219",
    base_url="https://api.anthropic.com/v1/",
    api_key="temp-api-key",
    max_tokens=21333,
    temperature=1,
    top_p=1,
    top_k=-1,
    model_info={
        "vision": True,
        "function_calling": True,
        "json_output": False,
        "family": "claude-3.7-sonnet",
    }
)



# ===Mini Transformers===
def _assert_valid_name(message: LLMMessage, context: Dict[str, Any]) -> Dict[str, Any]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great if we can have a typed dict for the context instead of a generic dict

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used explicit return type hints (e.g., Dict[str, str]) in each transformer to ensure key-level type safety and avoid cast().

Copy link
Contributor Author

@SongChiYoung SongChiYoung left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, Solved all of your require changes

elif isinstance(part, Image):
# TODO: support url based images
# TODO: support specifying details
parts.append(cast(ChatCompletionContentPartImageParam, part.to_openai_format()))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: That cast is still inside of prev code before my change

def user_message_to_oai(message: UserMessage, prepend_name: bool = False) -> ChatCompletionUserMessageParam:	def to_oai_type(
    assert_valid_name(message.source)	    message: LLMMessage, prepend_name: bool = False, model_family: str = "gpt-4o"
    if isinstance(message.content, str):	) -> Sequence[ChatCompletionMessageParam]:
        return ChatCompletionUserMessageParam(	    context = {
            content=(f"{message.source} said:\n" if prepend_name else "") + message.content,	        "prepend_name": prepend_name,
            role="user",	    }
            name=message.source,	    transformers = get_transformer("openai", model_family)
        )	
    else:	
        parts: List[ChatCompletionContentPartParam] = []	
        for part in message.content:	
            if isinstance(part, str):	
                if prepend_name:	
                    # Append the name to the first text part	
                    oai_part = ChatCompletionContentPartTextParam(	
                        text=f"{message.source} said:\n" + part,	
                        type="text",	
                    )	
                    prepend_name = False	
                else:	
                    oai_part = ChatCompletionContentPartTextParam(	
                        text=part,	
                        type="text",	
                    )	
                parts.append(oai_part)	
            elif isinstance(part, Image):	
                # TODO: support url based images	
                # TODO: support specifying details	
                parts.append(cast(ChatCompletionContentPartImageParam, part.to_openai_format()))
            else:	
                raise ValueError(f"Unknown content type: {part}")	
        return ChatCompletionUserMessageParam(	
            content=parts,	
            role="user",	
            name=message.source,	
        )	



# === Base Transformers list ===
base_system_message_transformers: List[Callable[[LLMMessage, Dict[str, Any]], Dict[str, Any]]] = [
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored to avoid cast() by enforcing TransformerFunc to return Sequence[MessageParam].
This preserves type safety while keeping the registry extensible beyond OpenAI.



# ===Mini Transformers===
def _assert_valid_name(message: LLMMessage, context: Dict[str, Any]) -> Dict[str, Any]:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used explicit return type hints (e.g., Dict[str, str]) in each transformer to ensure key-level type safety and avoid cast().

@SongChiYoung
Copy link
Contributor Author

SongChiYoung commented Mar 30, 2025

@ekzhu Done.

  • Addressed all requested changes
  • Resolved merge conflicts
  • Added documentation for the new design at the top of this PR (If you want to add it, let me know if there's a better place for it)

@a-holm
Copy link

a-holm commented Mar 30, 2025

One potential issue identified during review:

In python/packages/autogen-ext/src/autogen_ext/models/openai/_message_transform.py, the code defines a __CLAUDE_TRANSFORMER_MAP which includes the _set_empty_to_whitespace fix (similar to the Gemini map). However, the registration loop later in the file assigns the __BASE_TRANSFORMER_MAP (which lacks the fix) to Claude models (for model in __claude_models: loop). This seems to prevent the fix from being applied to Claude models when accessed via the OpenAI client interface. Could you please double-check if this registration is intended or if it should use __CLAUDE_TRANSFORMER_MAP instead?

It might be on purpose, just pointing it out if it isn't.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants