Skip to main content

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.

Source facts

Repository
Azure/azure-sdk-for-java
Last source activity
March 16, 2026 at 09:47
Detected SKILL.md language
English
Stars
2,567
Forks
2,181

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
dedup-openai
description
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.
# 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-*/`: ```bash find TempTypeSpecFiles -name "client*.tsp" -path "*/sdk-*" ``` Add `@@alternateType` directives for each actionable duplicate: ```tsp // 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: ```bash grep -rn "^model <ClassName>" TempTypeSpecFiles/node_modules/@azure-tools/openai-typespec/src/ --include="*.tsp" ``` ### 2. Regenerate and verify ```bash tsp-client generate ``` After generation, verify the suppressed classes are gone: ```bash # 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()`: ```java // 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()`: ```java // 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: ```java // 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](#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: ```java /** * 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: ```java // 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: ```java @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: 1. **Remove `@Generated`** from any method you modify. The codegen will not overwrite the method body. 2. **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. 3. **Place javadoc above the method** normally. Since the method lacks `@Generated`, the codegen preserves the javadoc you wrote. 4. **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. ```java // ✅ 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](#codegen-survival-rules)) | | Overloaded setters cause null ambiguity | `setFilters(null)` matches `BinaryData`, `ComparisonFilter`, and `CompoundFilter` | Use distinct method names: `setComparisonFilter()`, `setCompoundFilter()` |
View on GitHub