Skip to content
61 changes: 61 additions & 0 deletions sdk/ai/.workflow_docs/codegen/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
name: codegen
description: Generate code from TypeSpec via tsp-client (update, sync, generate). Requires a tsp-location.yaml in the current working directory. Supports updating the commit hash before running.
---

# TypeSpec Code Generation (tsp-client)

Use this skill to run `tsp-client` workflows for projects that include a `tsp-location.yaml` file.

## Preconditions
- You must be in the directory that contains `tsp-location.yaml`.
- If the file is missing, warn the user and ask for the correct directory (do not run commands).

## Commit hash update
If the user provides a commit hash, update the `commit:` field in `tsp-location.yaml` **before** running tsp-client.
- Read the file and locate the `commit:` line.
- Replace the value with the provided hash (keep the same key name and formatting).
- Example:
- Before: `commit: 6267b6...`
- After: `commit: <new_hash>`

## Commands

### `tsp-client update`
Pull the latest codegen tooling or definitions (default action when the user is vague).
```bash
tsp-client update
```

### `tsp-client sync`
Fetch/sync TypeSpec inputs for the project.
```bash
tsp-client sync
```

### `tsp-client generate`
Generate code from TypeSpec inputs.
```bash
tsp-client generate
```

Keep the synced TypeSpec inputs:
```bash
tsp-client generate --save-inputs
```

## Steps
1. Verify `tsp-location.yaml` exists in the current directory. If not, stop and ask for the correct location.
2. If the user provided a commit hash, update the `commit:` value in `tsp-location.yaml`.
3. Determine the user intent:
- **Refresh/update/ingest changes from a commit**: run `tsp-client update`.
- **Fetch/sync spec from the current commit**: run `tsp-client sync`.
- **Generate from fetched spec**: run `tsp-client generate` (use `--save-inputs` only if the user asks to keep inputs).
- **Generate (no fetch requested)**: run `tsp-client generate`.
4. If the user doesn’t specify, default to `tsp-client update`.
5. If the project defines or creates a `TempTypeSpecFiles` folder and the user wants code generation, run `tsp-client generate` (with `--save-inputs` if requested).
6. If a tsp-client command fails, report the error output and suggest checking the TypeSpec repo/commit referenced in `tsp-location.yaml`. Build a GitHub URL from `repo:` and `directory:` (and include the `commit:` as the ref), e.g.:
- Repo: `Azure/azure-rest-api-specs`
- Commit: `6267b6...`
- Directory: `specification/cognitiveservices/OpenAI.Inference`
- URL: `https://github.com/Azure/azure-rest-api-specs/tree/6267b6.../specification/cognitiveservices/OpenAI.Inference`
18 changes: 18 additions & 0 deletions sdk/ai/.workflow_docs/cspell.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import:
- ../../../.vscode/cspell.json
overrides:
- filename: "**/sdk/ai/.workflow_docs/*"
words:
- Danimal
- Dbuildhelper
- Dcheckstyle
- Dcodesnippet
- Djacoco
- dpkg
- Drevapi
- Dspotbugs
- Dspotless
- Dtest
- dedup
- FQCN
- javap
252 changes: 252 additions & 0 deletions sdk/ai/.workflow_docs/dedup-openai/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
---
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()` |
Loading