mcpbeat

Dedup Openai

azure/dedup-openai

Suppress generated Java classes that duplicate openai-java models, using @@alternateType in TypeSpec and manual serialization bridges. Use after dup-classes has identified actionable duplicates.

3k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
2566
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/Azure/azure-sdk-for-java --skill dedup-openai

The instruction itself

17 sections, as written by the author

De-duplicate Generated Classes Against openai-java

Use this skill after the dup-classes skill has identified actionable duplicates. This skill suppresses the generated classes and bridges to the openai-java equivalents.

Preconditions

  • A tsp-location.yaml must exist in the current directory.
  • TypeSpec must be synced (tsp-client sync) so TempTypeSpecFiles/ exists.
  • The openai-java dependency must be in the project's pom.xml.
  • You must know which classes to suppress (use dup-classes skill first).

Key concepts

What can be suppressed

Only standalone models that don't participate in a discriminator hierarchy. A model is standalone if:

  • It does NOT extend Tool, TextResponseFormatConfiguration, or another base class with a fromJson discriminator
  • It is NOT a subtype dispatched by a parent's fromJson method

What cannot be suppressed

Structural equivalents — classes that extend a base type in a discriminator hierarchy (e.g., FunctionTool extends Tool). The SDK's polymorphic serialization requires these. They produce identical JSON but are not actionable. Do NOT attempt to suppress them.

Two suppression mechanisms

| Mechanism | When to use | Effect |

|-----------|-------------|--------|

| @@alternateType(OpenAI.X, { identity: "com.openai.models.X" }, "java") | The model is referenced as a field type or union member in other models | Codegen replaces the type with the openai-java class. The generated class is NOT emitted at all. |

| @@access(OpenAI.X, Access.internal, "java") | The model is NOT referenced by any public model | Codegen moves the class to implementation.models. |

Prefer @@alternateType — it fully prevents emission and is the cleanest approach. Use @@access(internal) only as a supplement when a model isn't reachable through the type graph but is still emitted.

Why @@access(internal) alone may not work

If a model is referenced by a union or property in a public model, @@access(internal) will NOT move it. The codegen keeps it public because removing it would break the type graph. Example: ComparisonFilter is a member of the Filters union used by FileSearchTool.filters@@access(internal) has no effect, but @@alternateType prevents emission entirely.

Steps

1. Edit the TypeSpec client file

Locate the client.tsp (or client.java.tsp) in TempTypeSpecFiles/sdk-*/:

find TempTypeSpecFiles -name "client*.tsp" -path "*/sdk-*"

Add @@alternateType directives for each actionable duplicate:

// De-dup: map to openai-java equivalents
@@alternateType(OpenAI.ComparisonFilter, { identity: "com.openai.models.ComparisonFilter" }, "java");
@@alternateType(OpenAI.Reasoning, { identity: "com.openai.models.Reasoning" }, "java");

Finding the correct model name: The TypeSpec models are in the OpenAI namespace. Search the openai-typespec package:

grep -rn "^model <ClassName>" TempTypeSpecFiles/node_modules/@azure-tools/openai-typespec/src/ --include="*.tsp"

2. Regenerate and verify

tsp-client generate

After generation, verify the suppressed classes are gone:

# Should NOT exist:
ls src/main/java/com/azure/ai/agents/models/<SuppressedClass>.java
# Should NOT exist in implementation either (with @@alternateType):
ls src/main/java/com/azure/ai/agents/implementation/models/<SuppressedClass>.java

3. Fix serialization in parent models

When a model's property type changes from a generated JsonSerializable class to an openai-java class, the toJson/fromJson methods in parent models will break because the openai-java type doesn't implement JsonSerializable.

Pattern for toJson — use OpenAIJsonHelper.toBinaryData():

// Before (generated, won't compile):
jsonWriter.writeJsonField("reasoning", this.reasoning);

// After:
if (this.reasoning != null) {
    jsonWriter.writeFieldName("reasoning");
    OpenAIJsonHelper.toBinaryData(this.reasoning).writeTo(jsonWriter);
}

Pattern for fromJson — read as BinaryData, convert with OpenAIJsonHelper.fromBinaryData():

// Before (generated, won't compile):
reasoning = Reasoning.fromJson(reader);

// After:
BinaryData reasoningData
    = reader.getNullable(nonNullReader -> BinaryData.fromObject(nonNullReader.readUntyped()));
reasoning = OpenAIJsonHelper.fromBinaryData(reasoningData, com.openai.models.Reasoning.class);

Pattern for getter/setter — use the openai-java type directly, with javadoc above and marker comment inside the body:

// Field stores the openai-java type directly (no BinaryData indirection)
private com.openai.models.Reasoning reasoning; // AI Tooling: openai-java de-dup

/**
 * Gets the reasoning configuration.
 * @return the reasoning, or null if not set.
 */
public com.openai.models.Reasoning getReasoning() {
    // AI Tooling: openai-java de-dup
    return this.reasoning;
}

/**
 * Sets the reasoning configuration.
 * @param reasoning the reasoning to set.
 * @return this object.
 */
public PromptAgentDefinition setReasoning(com.openai.models.Reasoning reasoning) {
    // AI Tooling: openai-java de-dup
    this.reasoning = reasoning;
    return this;
}

Remove @Generated from any method you modify so the codegen preserves your changes on re-generation. See Codegen survival rules for comment/javadoc placement.

4. Add typed convenience setters (for BinaryData fields)

When a property is already BinaryData (e.g., because it's a union type), add distinctly named setter methods for the openai-java types. Do NOT overload setX with different parameter types — this causes null-ambiguity. Use descriptive names instead:

/**
 * Sets the filters using an openai-java ComparisonFilter.
 * @param filter the filter to apply, or null to clear.
 * @return this object.
 */
public FileSearchTool setComparisonFilter(com.openai.models.ComparisonFilter filter) {
    // AI Tooling: openai-java de-dup
    this.filters = OpenAIJsonHelper.toBinaryData(filter);
    return this;
}

/**
 * Sets the filters using an openai-java CompoundFilter.
 * @param filter the filter to apply, or null to clear.
 * @return this object.
 */
public FileSearchTool setCompoundFilter(com.openai.models.CompoundFilter filter) {
    // AI Tooling: openai-java de-dup
    this.filters = OpenAIJsonHelper.toBinaryData(filter);
    return this;
}

5. Add OpenAIJsonHelper methods if needed

The OpenAIJsonHelper class in com.azure.ai.agents.implementation may need two bridge methods:

// Serialize openai-java object → BinaryData (writes as JSON object, not quoted string)
public static BinaryData toBinaryData(Object openAIObject)

// Deserialize BinaryData → openai-java type
public static <T> T fromBinaryData(BinaryData data, Class<T> type)

These use the openai-java ObjectMappers.jsonMapper() (which handles Kotlin internals correctly). Do NOT use BinaryData.fromObject() or BinaryData.toObject() with openai-java types — the default Jackson ObjectMapper cannot serialize Kotlin SynchronizedLazyImpl fields.

6. Write serialization tests

Write round-trip tests verifying the JSON shape is preserved. Test pattern:

@Test
public void testRoundTrip() throws IOException {
    // Build with openai-java type
    Reasoning reasoning = Reasoning.builder().effort(ReasoningEffort.HIGH).build();
    PromptAgentDefinition original = new PromptAgentDefinition("gpt-4o").setReasoning(reasoning);

    // Serialize
    String json = serialize(original);
    assertTrue(json.contains("\"effort\":\"high\""));

    // Deserialize
    PromptAgentDefinition deserialized = deserialize(json);
    assertEquals(ReasoningEffort.HIGH, deserialized.getReasoning().effort().get());

    // Re-serialize and compare
    assertEquals(json, serialize(deserialized));
}

Cover: all enum values, null/absent fields, combined with other fields, polymorphic deserialization via parent fromJson.

7. Apply changes to the spec repo

If a local checkout of Azure/azure-rest-api-specs is available, apply the same client.tsp edits there. Derive the path from tsp-location.yaml:

<spec_repo>/<directory>/client.tsp

Codegen survival rules

The TypeSpec Java codegen (tsp-client update / tsp-client generate) will re-generate files on every run. Methods without @Generated are preserved (body intact), but everything above the method signature (javadoc, comments) is regenerated. Follow these rules so your manual edits survive:

  • Remove @Generated from any method you modify. The codegen will not overwrite the method body.
  • Place marker comments inside the method body, not above the signature. The codegen rewrites the javadoc block above the signature but does not touch the body.
  • Place javadoc above the method normally. Since the method lacks @Generated, the codegen preserves the javadoc you wrote.
  • For field declarations, place marker comments on the same line (trailing), not on the line above. The codegen regenerates the comment block above the field.
// ✅ SURVIVES codegen: javadoc above, marker inside body
/**
 * Gets the reasoning configuration.
 * @return the reasoning, or null if not set.
 */
public com.openai.models.Reasoning getReasoning() {
    // AI Tooling: openai-java de-dup  ← inside body, survives
    return this.reasoning;
}

// ❌ WIPED by codegen: marker above signature
// AI Tooling: openai-java de-dup  ← above signature, gets wiped
public com.openai.models.Reasoning getReasoning() {
    return this.reasoning;
}

// ✅ SURVIVES codegen: field marker on same line
private com.openai.models.Reasoning reasoning; // AI Tooling: openai-java de-dup

// ❌ WIPED by codegen: field marker on line above
// AI Tooling: openai-java de-dup
private com.openai.models.Reasoning reasoning;

Common pitfalls

| Problem | Cause | Fix |

|---------|-------|-----|

| Class stays public despite @@access(internal) | Referenced by a union or property in a public model | Use @@alternateType instead |

| BinaryData.fromObject(openAIObj) throws SynchronizedLazyImpl error | Default Jackson can't serialize Kotlin internals | Use OpenAIJsonHelper.toBinaryData() which uses ObjectMappers.jsonMapper() |

| BinaryData.fromString(json).writeTo(writer) writes quoted string | fromString creates text content, not JSON | Use BinaryData.fromObject(reader.readUntyped()) to store as a JSON object |

| Getter/setter bridge through BinaryData on every call | Unnecessary indirection | Store the openai-java type directly in the field; bridge only in toJson/fromJson |

| Tried to suppress a Tool subclass | Structural equivalent, not an actionable duplicate | Don't suppress — it's needed for polymorphic deserialization |

| Javadoc/comments above method wiped after codegen | Codegen rewrites everything above non-@Generated method signatures | Place marker comments inside the method body; javadoc survives if @Generated is removed (see Codegen survival rules) |

| Overloaded setters cause null ambiguity | setFilters(null) matches BinaryData, ComparisonFilter, and CompoundFilter | Use distinct method names: setComparisonFilter(), setCompoundFilter() |

How to use it

Copy the folder

Take azure/dedup-openai from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.