From 9dbda1e9b4b37d083a3ff39bd84e9a57bf1049da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:25:56 +0000 Subject: [PATCH 01/18] Initial plan From 98b33dea38a8736a6b88a3ec8db79709ac993149 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:29:19 +0000 Subject: [PATCH 02/18] Update multiple-services.md design doc with extended client hierarchy customization Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> --- .../design-docs/multiple-services.md | 368 ++++++++++++++++++ 1 file changed, 368 insertions(+) diff --git a/packages/typespec-client-generator-core/design-docs/multiple-services.md b/packages/typespec-client-generator-core/design-docs/multiple-services.md index 17bd69edcf..2de078a2d5 100644 --- a/packages/typespec-client-generator-core/design-docs/multiple-services.md +++ b/packages/typespec-client-generator-core/design-docs/multiple-services.md @@ -250,3 +250,371 @@ The resulting `SharedGroup` operation group will have: - `apiVersions: []` - API version parameter with `type.kind === "string"` - Operations from both ServiceA and ServiceB + +## Extended Design: Advanced Client Hierarchy Customization + +The first step design focuses on automatically merging multiple services into one client. However, users have requested more flexibility in how they organize clients from multiple services. This section extends the previous [client hierarchy design](./client.md) to provide three additional scenarios: + +1. Define multiple clients, each belonging to one service +2. Do not auto-merge nested namespaces/interfaces into the root client, instead merge them as direct children of the root client +3. Fully customize how operations from different services are combined into different client hierarchies + +### Scenario 1: Multiple Clients, Each Belonging to One Service + +In some cases, users may want to generate separate clients for each service rather than combining them into one client. This is useful when services have different authentication, versioning, or other client-level settings. + +#### Syntax Proposal + +Define multiple `@client` decorators, each targeting one service: + +```typespec title="main.tsp" +@service +@versioned(VersionsA) +namespace ServiceA { + enum VersionsA { av1, av2 } + + interface Operations { + opA(): void; + } + + namespace SubNamespace { + op subOpA(): void; + } +} + +@service +@versioned(VersionsB) +namespace ServiceB { + enum VersionsB { bv1, bv2 } + + interface Operations { + opB(): void; + } + + namespace SubNamespace { + op subOpB(): void; + } +} +``` + +```typespec title="client.tsp" +import "./main.tsp"; +import "@azure-tools/typespec-client-generator-core"; + +using Azure.ClientGenerator.Core; + +@client({ + name: "ServiceAClient", + service: ServiceA, +}) +namespace ServiceAClient; + +@client({ + name: "ServiceBClient", + service: ServiceB, +}) +namespace ServiceBClient; +``` + +#### TCGC Behavior + +This creates two independent root clients, each with their own service hierarchy: + +```yaml +clients: + - kind: client + name: ServiceAClient + apiVersions: [av1, av2] + subClients: + - kind: client + name: Operations + # operations: [opA] + - kind: client + name: SubNamespace + # operations: [subOpA] + - kind: client + name: ServiceBClient + apiVersions: [bv1, bv2] + subClients: + - kind: client + name: Operations + # operations: [opB] + - kind: client + name: SubNamespace + # operations: [subOpB] +``` + +### Scenario 2: Merging Services as Direct Children (No Deep Auto-Merge) + +In the first step design, when combining multiple services, all nested namespaces/interfaces are auto-merged into the root client as sub-clients. Some users prefer to keep each service's namespace as a direct child of the root client without deep merging. + +#### Syntax Proposal: The `autoMerge` Option + +We introduce a new `autoMerge` option in the `@client` decorator to control whether the service's content should be automatically merged into the current client: + +```typespec +@client({ + name: "CombineClient", + service: [ServiceA, ServiceB], + autoMerge: false, // NEW OPTION +}) +namespace CombineClient; +``` + +**Option Values:** + +- `autoMerge: true` (default): Behaves like the first step design. All nested namespaces/interfaces from all services are merged into the root client as sub-clients. +- `autoMerge: false`: Each service's namespace becomes a direct sub-client of the root client. The nested namespaces/interfaces remain under their respective service sub-client. + +#### Example + +Given the same services from Scenario 1: + +```typespec title="client.tsp" +import "./main.tsp"; +import "@azure-tools/typespec-client-generator-core"; + +using Azure.ClientGenerator.Core; + +@client({ + name: "CombineClient", + service: [ServiceA, ServiceB], + autoMerge: false, +}) +@useDependency(ServiceA.VersionsA.av2, ServiceB.VersionsB.bv2) +namespace CombineClient; +``` + +#### TCGC Behavior with `autoMerge: false` + +When `autoMerge` is `false`, TCGC will: + +1. Create the root client for the combined client (same as first step design). +2. Create a sub-client for each service namespace. Each service sub-client contains all the nested namespaces/interfaces as its own sub-clients. +3. Operations directly under each service's namespace are placed under the service sub-client, not the root client. +4. No automatic merging of same-named namespaces/interfaces across services occurs. + +```yaml +clients: + - &root + kind: client + name: CombineClient + apiVersions: [] + clientInitialization: + initializedBy: individually + subClients: + - &svcA + kind: client + name: ServiceA + parent: *root + apiVersions: [av1, av2] + clientInitialization: + initializedBy: parent + subClients: + - kind: client + name: Operations + parent: *svcA + # operations: [opA] + - kind: client + name: SubNamespace + parent: *svcA + # operations: [subOpA] + - &svcB + kind: client + name: ServiceB + parent: *root + apiVersions: [bv1, bv2] + clientInitialization: + initializedBy: parent + subClients: + - kind: client + name: Operations + parent: *svcB + # operations: [opB] + - kind: client + name: SubNamespace + parent: *svcB + # operations: [subOpB] +``` + +#### Python SDK Example + +With `autoMerge: false`: + +```python +client = CombineClient(endpoint="endpoint", credential=AzureKeyCredential("key")) + +# Access ServiceA operations +client.service_a.operations.op_a() +client.service_a.sub_namespace.sub_op_a() + +# Access ServiceB operations +client.service_b.operations.op_b() +client.service_b.sub_namespace.sub_op_b() +``` + +Compared to `autoMerge: true` (first step design): + +```python +client = CombineClient(endpoint="endpoint", credential=AzureKeyCredential("key")) + +# ServiceA and ServiceB namespaces are auto-merged +client.operations.op_a() # Note: This would conflict with opB in same-named interface +client.operations.op_b() +client.sub_namespace.sub_op_a() +client.sub_namespace.sub_op_b() +``` + +### Scenario 3: Fully Customized Client Hierarchy + +For maximum flexibility, users can fully customize how operations from different services are organized into client hierarchies. This extends the `@client` decorator to support explicit operation mapping across services. + +#### Syntax Proposal + +Use nested `@client` decorators with explicit operation references to create a custom client hierarchy: + +```typespec title="client.tsp" +import "./main.tsp"; +import "@azure-tools/typespec-client-generator-core"; + +using Azure.ClientGenerator.Core; + +@client({ + name: "CustomClient", + service: [ServiceA, ServiceB], + autoMerge: false, +}) +namespace CustomClient { + // Custom sub-client combining operations from both services + @client + interface SharedOperations { + opA is ServiceA.Operations.opA; + opB is ServiceB.Operations.opB; + } + + // Custom sub-client with operations from ServiceA only + @client + interface ServiceAOnly { + subOpA is ServiceA.SubNamespace.subOpA; + } + + // Custom sub-client with operations from ServiceB only + @client + interface ServiceBOnly { + subOpB is ServiceB.SubNamespace.subOpB; + } +} +``` + +#### TCGC Behavior + +When explicit `@client` decorators are nested within the root client: + +1. TCGC uses the explicitly defined client hierarchy instead of auto-generating from service structure. +2. Each nested `@client` becomes a sub-client of the root client. +3. Operations referenced via `is` keyword are mapped to their original service operations. +4. The `autoMerge: false` option ensures that only explicitly defined operations are included; no auto-discovery from service namespaces occurs. + +```yaml +clients: + - &root + kind: client + name: CustomClient + apiVersions: [] + subClients: + - kind: client + name: SharedOperations + parent: *root + apiVersions: [] # Mixed from multiple services + methods: + - kind: basic + name: opA + # service: ServiceA + - kind: basic + name: opB + # service: ServiceB + - kind: client + name: ServiceAOnly + parent: *root + apiVersions: [av1, av2] + methods: + - kind: basic + name: subOpA + - kind: client + name: ServiceBOnly + parent: *root + apiVersions: [bv1, bv2] + methods: + - kind: basic + name: subOpB +``` + +#### Python SDK Example + +```python +client = CustomClient(endpoint="endpoint", credential=AzureKeyCredential("key")) + +# Access shared operations from both services +client.shared_operations.op_a() # Uses ServiceA's API version +client.shared_operations.op_b() # Uses ServiceB's API version + +# Access ServiceA-only operations +client.service_a_only.sub_op_a() + +# Access ServiceB-only operations +client.service_b_only.sub_op_b() +``` + +### Summary of `autoMerge` Behavior + +| Scenario | `autoMerge` | Explicit `@client` | Result | +| -------------------- | ---------------- | ------------------ | ---------------------------------------------------------------------- | +| First step design | `true` (default) | No | All services' nested items merged as root client's sub-clients | +| Services as children | `false` | No | Each service namespace becomes a sub-client, keeping its own hierarchy | +| Fully customized | `false` | Yes | Only explicitly defined clients and operations are used | +| Partially customized | `true` | Yes | Explicit clients used, plus remaining service content auto-merged | + +### Interaction with Existing Decorators + +The `autoMerge` option works alongside existing customization decorators: + +- **`@clientInitialization`**: Still controls how each client is initialized. Can be applied to both auto-merged and explicitly defined clients. +- **`@clientLocation`**: Can move operations between clients regardless of `autoMerge` setting. +- **`@operationGroup`** (deprecated): Replaced by nested `@client`. Migration path: convert `@operationGroup` to `@client` with `autoMerge: false`. + +### Validation Rules + +1. When `autoMerge: false` is used without explicit nested `@client` decorators: + - Each service namespace becomes a sub-client automatically + - No deep merging of same-named namespaces/interfaces occurs + +2. When `autoMerge: false` is used with explicit nested `@client` decorators: + - Only explicitly defined clients are created + - Operations not referenced by any explicit client are omitted from the SDK + - A diagnostic warning is issued for unreferenced operations + +3. When `autoMerge: true` (default) is used with explicit nested `@client` decorators: + - Explicit clients take precedence + - Remaining operations from services are auto-merged into additional sub-clients + +4. The `autoMerge` option is only valid when `service` is an array with multiple services: + - For single-service clients, `autoMerge` has no effect (there's nothing to merge) + +### Changes Needed + +1. **Update `@client` decorator options**: + - Add `autoMerge?: boolean` property to `ClientOptions` type + - Default value is `true` to maintain backward compatibility + +2. **Update `cache.ts` logic**: + - Check `autoMerge` option when processing multiple services + - When `false`, create service-level sub-clients instead of merging + - Handle explicit nested `@client` decorators within multi-service clients + +3. **Update client types**: + - Add `autoMerge` property to `SdkClient` to track the configuration + - Ensure `subClients` structure reflects the chosen merge strategy + +4. **Add validation**: + - Emit diagnostic if `autoMerge: false` with explicit clients has unreferenced operations + - Emit diagnostic if `autoMerge` is used with single-service client (no-op warning) From cb08ae31cc3c438929d66ed938b8ca29af315524 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:30:11 +0000 Subject: [PATCH 03/18] Address code review comments on design doc Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> --- .../design-docs/multiple-services.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/typespec-client-generator-core/design-docs/multiple-services.md b/packages/typespec-client-generator-core/design-docs/multiple-services.md index 2de078a2d5..95b4f90f71 100644 --- a/packages/typespec-client-generator-core/design-docs/multiple-services.md +++ b/packages/typespec-client-generator-core/design-docs/multiple-services.md @@ -580,7 +580,7 @@ The `autoMerge` option works alongside existing customization decorators: - **`@clientInitialization`**: Still controls how each client is initialized. Can be applied to both auto-merged and explicitly defined clients. - **`@clientLocation`**: Can move operations between clients regardless of `autoMerge` setting. -- **`@operationGroup`** (deprecated): Replaced by nested `@client`. Migration path: convert `@operationGroup` to `@client` with `autoMerge: false`. +- **`@operationGroup`** (deprecated): The same functionality can be achieved using nested `@client`. Migration path: convert `@operationGroup` to nested `@client`, optionally combined with `autoMerge: false` for multi-service scenarios where you want to prevent deep merging. ### Validation Rules @@ -597,8 +597,8 @@ The `autoMerge` option works alongside existing customization decorators: - Explicit clients take precedence - Remaining operations from services are auto-merged into additional sub-clients -4. The `autoMerge` option is only valid when `service` is an array with multiple services: - - For single-service clients, `autoMerge` has no effect (there's nothing to merge) +4. The `autoMerge` option is only meaningful when `service` is an array with multiple services: + - For single-service clients, `autoMerge` is ignored (there's nothing to merge from multiple services) ### Changes Needed From b39f412ee5826db9be4ad534f1c5f4cd6c3c72ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:30:34 +0000 Subject: [PATCH 04/18] Fix inconsistency in validation rules section Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> --- .../design-docs/multiple-services.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/typespec-client-generator-core/design-docs/multiple-services.md b/packages/typespec-client-generator-core/design-docs/multiple-services.md index 95b4f90f71..2eccd5bd78 100644 --- a/packages/typespec-client-generator-core/design-docs/multiple-services.md +++ b/packages/typespec-client-generator-core/design-docs/multiple-services.md @@ -617,4 +617,3 @@ The `autoMerge` option works alongside existing customization decorators: 4. **Add validation**: - Emit diagnostic if `autoMerge: false` with explicit clients has unreferenced operations - - Emit diagnostic if `autoMerge` is used with single-service client (no-op warning) From f20ed7f0f3b8e855eec111bdaf697d60b718f562 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 08:17:11 +0000 Subject: [PATCH 05/18] Refine design to remove autoMerge flag, use nested @client pattern instead Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> --- .../design-docs/multiple-services.md | 214 +++++++++--------- 1 file changed, 102 insertions(+), 112 deletions(-) diff --git a/packages/typespec-client-generator-core/design-docs/multiple-services.md b/packages/typespec-client-generator-core/design-docs/multiple-services.md index 2eccd5bd78..f571f1f340 100644 --- a/packages/typespec-client-generator-core/design-docs/multiple-services.md +++ b/packages/typespec-client-generator-core/design-docs/multiple-services.md @@ -253,11 +253,12 @@ The resulting `SharedGroup` operation group will have: ## Extended Design: Advanced Client Hierarchy Customization -The first step design focuses on automatically merging multiple services into one client. However, users have requested more flexibility in how they organize clients from multiple services. This section extends the previous [client hierarchy design](./client.md) to provide three additional scenarios: +The first step design focuses on automatically merging multiple services into one client when the client namespace is empty. However, users have requested more flexibility in how they organize clients from multiple services. This section extends the previous [client hierarchy design](./client.md) to provide additional scenarios by leveraging nested `@client` decorators. -1. Define multiple clients, each belonging to one service -2. Do not auto-merge nested namespaces/interfaces into the root client, instead merge them as direct children of the root client -3. Fully customize how operations from different services are combined into different client hierarchies +The key design principle is: + +- **If the client namespace is empty**: TCGC auto-merges all services' nested namespaces/interfaces into the current client as children (first step design behavior). +- **If the client namespace has inner defined content** (nested `@client` decorators): TCGC uses the explicitly defined client hierarchy instead of auto-merging. ### Scenario 1: Multiple Clients, Each Belonging to One Service @@ -325,50 +326,40 @@ clients: - kind: client name: ServiceAClient apiVersions: [av1, av2] - subClients: + children: - kind: client name: Operations - # operations: [opA] + methods: + - kind: basic + name: opA - kind: client name: SubNamespace - # operations: [subOpA] + methods: + - kind: basic + name: subOpA - kind: client name: ServiceBClient apiVersions: [bv1, bv2] - subClients: + children: - kind: client name: Operations - # operations: [opB] + methods: + - kind: basic + name: opB - kind: client name: SubNamespace - # operations: [subOpB] -``` - -### Scenario 2: Merging Services as Direct Children (No Deep Auto-Merge) - -In the first step design, when combining multiple services, all nested namespaces/interfaces are auto-merged into the root client as sub-clients. Some users prefer to keep each service's namespace as a direct child of the root client without deep merging. - -#### Syntax Proposal: The `autoMerge` Option - -We introduce a new `autoMerge` option in the `@client` decorator to control whether the service's content should be automatically merged into the current client: - -```typespec -@client({ - name: "CombineClient", - service: [ServiceA, ServiceB], - autoMerge: false, // NEW OPTION -}) -namespace CombineClient; + methods: + - kind: basic + name: subOpB ``` -**Option Values:** +### Scenario 2: Services as Direct Children (No Deep Auto-Merge) -- `autoMerge: true` (default): Behaves like the first step design. All nested namespaces/interfaces from all services are merged into the root client as sub-clients. -- `autoMerge: false`: Each service's namespace becomes a direct sub-client of the root client. The nested namespaces/interfaces remain under their respective service sub-client. +In the first step design, when combining multiple services with an empty client namespace, all nested namespaces/interfaces from all services are auto-merged into the root client as children. Some users prefer to keep each service's namespace as a direct child of the root client without deep merging. -#### Example +#### Syntax Proposal -Given the same services from Scenario 1: +Use nested `@client` decorators to explicitly define each service as a child client: ```typespec title="client.tsp" import "./main.tsp"; @@ -379,20 +370,30 @@ using Azure.ClientGenerator.Core; @client({ name: "CombineClient", service: [ServiceA, ServiceB], - autoMerge: false, }) -@useDependency(ServiceA.VersionsA.av2, ServiceB.VersionsB.bv2) -namespace CombineClient; +namespace CombineClient { + @client({ + name: "ComputeClient", + service: ServiceA, + }) + namespace Compute; + + @client({ + name: "DiskClient", + service: ServiceB, + }) + namespace Disk; +} ``` -#### TCGC Behavior with `autoMerge: false` +#### TCGC Behavior -When `autoMerge` is `false`, TCGC will: +When the client namespace has nested `@client` decorators, TCGC will use the explicitly defined client hierarchy: -1. Create the root client for the combined client (same as first step design). -2. Create a sub-client for each service namespace. Each service sub-client contains all the nested namespaces/interfaces as its own sub-clients. -3. Operations directly under each service's namespace are placed under the service sub-client, not the root client. -4. No automatic merging of same-named namespaces/interfaces across services occurs. +1. Create the root client for the combined client. +2. Each nested `@client` becomes a child of the root client. +3. Since the nested client namespaces (`Compute` and `Disk`) are empty, TCGC auto-merges each service's content into its respective child client. +4. No automatic merging across services occurs at the root level. ```yaml clients: @@ -402,63 +403,69 @@ clients: apiVersions: [] clientInitialization: initializedBy: individually - subClients: - - &svcA + children: + - &compute kind: client - name: ServiceA + name: ComputeClient parent: *root apiVersions: [av1, av2] clientInitialization: initializedBy: parent - subClients: + children: - kind: client name: Operations - parent: *svcA - # operations: [opA] + parent: *compute + methods: + - kind: basic + name: opA - kind: client name: SubNamespace - parent: *svcA - # operations: [subOpA] - - &svcB + parent: *compute + methods: + - kind: basic + name: subOpA + - &disk kind: client - name: ServiceB + name: DiskClient parent: *root apiVersions: [bv1, bv2] clientInitialization: initializedBy: parent - subClients: + children: - kind: client name: Operations - parent: *svcB - # operations: [opB] + parent: *disk + methods: + - kind: basic + name: opB - kind: client name: SubNamespace - parent: *svcB - # operations: [subOpB] + parent: *disk + methods: + - kind: basic + name: subOpB ``` #### Python SDK Example -With `autoMerge: false`: - ```python client = CombineClient(endpoint="endpoint", credential=AzureKeyCredential("key")) -# Access ServiceA operations -client.service_a.operations.op_a() -client.service_a.sub_namespace.sub_op_a() +# Access ServiceA operations via ComputeClient +client.compute.operations.op_a() +client.compute.sub_namespace.sub_op_a() -# Access ServiceB operations -client.service_b.operations.op_b() -client.service_b.sub_namespace.sub_op_b() +# Access ServiceB operations via DiskClient +client.disk.operations.op_b() +client.disk.sub_namespace.sub_op_b() ``` -Compared to `autoMerge: true` (first step design): +Compared to the first step design (empty namespace, auto-merge): ```python client = CombineClient(endpoint="endpoint", credential=AzureKeyCredential("key")) -# ServiceA and ServiceB namespaces are auto-merged +# ServiceA and ServiceB namespaces are auto-merged at root level client.operations.op_a() # Note: This would conflict with opB in same-named interface client.operations.op_b() client.sub_namespace.sub_op_a() @@ -467,7 +474,7 @@ client.sub_namespace.sub_op_b() ### Scenario 3: Fully Customized Client Hierarchy -For maximum flexibility, users can fully customize how operations from different services are organized into client hierarchies. This extends the `@client` decorator to support explicit operation mapping across services. +For maximum flexibility, users can fully customize how operations from different services are organized into client hierarchies. This uses nested `@client` decorators with explicit operation mapping. #### Syntax Proposal @@ -482,23 +489,22 @@ using Azure.ClientGenerator.Core; @client({ name: "CustomClient", service: [ServiceA, ServiceB], - autoMerge: false, }) namespace CustomClient { - // Custom sub-client combining operations from both services + // Custom child client combining operations from both services @client interface SharedOperations { opA is ServiceA.Operations.opA; opB is ServiceB.Operations.opB; } - // Custom sub-client with operations from ServiceA only + // Custom child client with operations from ServiceA only @client interface ServiceAOnly { subOpA is ServiceA.SubNamespace.subOpA; } - // Custom sub-client with operations from ServiceB only + // Custom child client with operations from ServiceB only @client interface ServiceBOnly { subOpB is ServiceB.SubNamespace.subOpB; @@ -511,9 +517,9 @@ namespace CustomClient { When explicit `@client` decorators are nested within the root client: 1. TCGC uses the explicitly defined client hierarchy instead of auto-generating from service structure. -2. Each nested `@client` becomes a sub-client of the root client. +2. Each nested `@client` becomes a child of the root client. 3. Operations referenced via `is` keyword are mapped to their original service operations. -4. The `autoMerge: false` option ensures that only explicitly defined operations are included; no auto-discovery from service namespaces occurs. +4. Since the root client namespace has inner content, no auto-discovery from service namespaces occurs. ```yaml clients: @@ -521,7 +527,7 @@ clients: kind: client name: CustomClient apiVersions: [] - subClients: + children: - kind: client name: SharedOperations parent: *root @@ -529,10 +535,8 @@ clients: methods: - kind: basic name: opA - # service: ServiceA - kind: basic name: opB - # service: ServiceB - kind: client name: ServiceAOnly parent: *root @@ -565,55 +569,41 @@ client.service_a_only.sub_op_a() client.service_b_only.sub_op_b() ``` -### Summary of `autoMerge` Behavior +### Summary of Client Hierarchy Behavior -| Scenario | `autoMerge` | Explicit `@client` | Result | -| -------------------- | ---------------- | ------------------ | ---------------------------------------------------------------------- | -| First step design | `true` (default) | No | All services' nested items merged as root client's sub-clients | -| Services as children | `false` | No | Each service namespace becomes a sub-client, keeping its own hierarchy | -| Fully customized | `false` | Yes | Only explicitly defined clients and operations are used | -| Partially customized | `true` | Yes | Explicit clients used, plus remaining service content auto-merged | +| Scenario | Client Namespace Content | Result | +| -------------------------- | ------------------------ | ---------------------------------------------------------------------- | +| First step design | Empty | All services' nested items auto-merged as root client's children | +| Services as children | Nested `@client` (empty) | Each nested client auto-merges its service's content | +| Fully customized | Nested `@client` with ops| Only explicitly defined clients and operations are used | ### Interaction with Existing Decorators -The `autoMerge` option works alongside existing customization decorators: +The nested `@client` approach works alongside existing customization decorators: - **`@clientInitialization`**: Still controls how each client is initialized. Can be applied to both auto-merged and explicitly defined clients. -- **`@clientLocation`**: Can move operations between clients regardless of `autoMerge` setting. -- **`@operationGroup`** (deprecated): The same functionality can be achieved using nested `@client`. Migration path: convert `@operationGroup` to nested `@client`, optionally combined with `autoMerge: false` for multi-service scenarios where you want to prevent deep merging. +- **`@clientLocation`**: Can move operations between clients regardless of namespace content. +- **`@operationGroup`** (deprecated): The same functionality can be achieved using nested `@client`. Migration path: convert `@operationGroup` to nested `@client`. ### Validation Rules -1. When `autoMerge: false` is used without explicit nested `@client` decorators: - - Each service namespace becomes a sub-client automatically - - No deep merging of same-named namespaces/interfaces occurs +1. When the root client namespace is empty: + - All services' content is auto-merged into the root client + - Same-named namespaces/interfaces across services are merged together -2. When `autoMerge: false` is used with explicit nested `@client` decorators: - - Only explicitly defined clients are created +2. When the root client namespace has nested `@client` decorators: + - Only explicitly defined clients are created at the root level + - Each nested `@client` with an empty namespace auto-merges its service's content + - Each nested `@client` with explicit operations uses only those operations - Operations not referenced by any explicit client are omitted from the SDK - A diagnostic warning is issued for unreferenced operations -3. When `autoMerge: true` (default) is used with explicit nested `@client` decorators: - - Explicit clients take precedence - - Remaining operations from services are auto-merged into additional sub-clients - -4. The `autoMerge` option is only meaningful when `service` is an array with multiple services: - - For single-service clients, `autoMerge` is ignored (there's nothing to merge from multiple services) - ### Changes Needed -1. **Update `@client` decorator options**: - - Add `autoMerge?: boolean` property to `ClientOptions` type - - Default value is `true` to maintain backward compatibility - -2. **Update `cache.ts` logic**: - - Check `autoMerge` option when processing multiple services - - When `false`, create service-level sub-clients instead of merging - - Handle explicit nested `@client` decorators within multi-service clients - -3. **Update client types**: - - Add `autoMerge` property to `SdkClient` to track the configuration - - Ensure `subClients` structure reflects the chosen merge strategy +1. **Update `cache.ts` logic**: + - Check if the client namespace has nested `@client` decorators + - When nested `@client` decorators exist, use the explicitly defined hierarchy + - When the namespace is empty, auto-merge services' content (existing behavior) -4. **Add validation**: - - Emit diagnostic if `autoMerge: false` with explicit clients has unreferenced operations +2. **Add validation**: + - Emit diagnostic if explicit clients have unreferenced operations from the services From 30c01d2615b6ce47b1c8da683f47de249378ff5c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 08:20:22 +0000 Subject: [PATCH 06/18] Fix minor wording and clarity issues from code review Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> --- .../design-docs/multiple-services.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/typespec-client-generator-core/design-docs/multiple-services.md b/packages/typespec-client-generator-core/design-docs/multiple-services.md index f571f1f340..716b87c99f 100644 --- a/packages/typespec-client-generator-core/design-docs/multiple-services.md +++ b/packages/typespec-client-generator-core/design-docs/multiple-services.md @@ -258,7 +258,7 @@ The first step design focuses on automatically merging multiple services into on The key design principle is: - **If the client namespace is empty**: TCGC auto-merges all services' nested namespaces/interfaces into the current client as children (first step design behavior). -- **If the client namespace has inner defined content** (nested `@client` decorators): TCGC uses the explicitly defined client hierarchy instead of auto-merging. +- **If the client namespace contains nested `@client` decorators**: TCGC uses the explicitly defined client hierarchy instead of auto-merging. ### Scenario 1: Multiple Clients, Each Belonging to One Service @@ -466,7 +466,7 @@ Compared to the first step design (empty namespace, auto-merge): client = CombineClient(endpoint="endpoint", credential=AzureKeyCredential("key")) # ServiceA and ServiceB namespaces are auto-merged at root level -client.operations.op_a() # Note: This would conflict with opB in same-named interface +client.operations.op_a() # Both opA and opB are in same merged Operations group client.operations.op_b() client.sub_namespace.sub_op_a() client.sub_namespace.sub_op_b() @@ -519,7 +519,7 @@ When explicit `@client` decorators are nested within the root client: 1. TCGC uses the explicitly defined client hierarchy instead of auto-generating from service structure. 2. Each nested `@client` becomes a child of the root client. 3. Operations referenced via `is` keyword are mapped to their original service operations. -4. Since the root client namespace has inner content, no auto-discovery from service namespaces occurs. +4. Since the root client namespace contains nested `@client` decorators, no auto-discovery from service namespaces occurs. ```yaml clients: @@ -531,7 +531,7 @@ clients: - kind: client name: SharedOperations parent: *root - apiVersions: [] # Mixed from multiple services + apiVersions: [] # Empty because operations come from different services with different versioning methods: - kind: basic name: opA @@ -571,11 +571,11 @@ client.service_b_only.sub_op_b() ### Summary of Client Hierarchy Behavior -| Scenario | Client Namespace Content | Result | -| -------------------------- | ------------------------ | ---------------------------------------------------------------------- | -| First step design | Empty | All services' nested items auto-merged as root client's children | -| Services as children | Nested `@client` (empty) | Each nested client auto-merges its service's content | -| Fully customized | Nested `@client` with ops| Only explicitly defined clients and operations are used | +| Scenario | Client Namespace Content | Result | +| -------------------------- | ------------------------------------- | ---------------------------------------------------------------------- | +| First step design | Empty | All services' nested items auto-merged as root client's children | +| Services as children | Nested `@client` (empty namespaces) | Each nested client auto-merges its service's content | +| Fully customized | Nested `@client` with explicit ops | Only explicitly defined clients and operations are used | ### Interaction with Existing Decorators From 83e919903adb26c32280febebacb0e06355b568e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 09:19:15 +0000 Subject: [PATCH 07/18] Address PR review comments: add Python examples, add clientInitialization to all YAML, remove comparison code Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> --- .../design-docs/multiple-services.md | 70 ++++++++++++++----- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/packages/typespec-client-generator-core/design-docs/multiple-services.md b/packages/typespec-client-generator-core/design-docs/multiple-services.md index 716b87c99f..62258563ca 100644 --- a/packages/typespec-client-generator-core/design-docs/multiple-services.md +++ b/packages/typespec-client-generator-core/design-docs/multiple-services.md @@ -321,38 +321,72 @@ namespace ServiceBClient; This creates two independent root clients, each with their own service hierarchy: +According to [client.md](./client.md), the default value of `initializedBy` for a root client is `InitializedBy.individually`, while for a sub client it is `InitializedBy.parent`. + ```yaml clients: - - kind: client + - &a1 + kind: client name: ServiceAClient apiVersions: [av1, av2] + clientInitialization: + initializedBy: individually children: - kind: client name: Operations + parent: *a1 + clientInitialization: + initializedBy: parent methods: - kind: basic name: opA - kind: client name: SubNamespace + parent: *a1 + clientInitialization: + initializedBy: parent methods: - kind: basic name: subOpA - - kind: client + - &a2 + kind: client name: ServiceBClient apiVersions: [bv1, bv2] + clientInitialization: + initializedBy: individually children: - kind: client name: Operations + parent: *a2 + clientInitialization: + initializedBy: parent methods: - kind: basic name: opB - kind: client name: SubNamespace + parent: *a2 + clientInitialization: + initializedBy: parent methods: - kind: basic name: subOpB ``` +#### Python SDK Example + +```python +# ServiceA client +client_a = ServiceAClient(endpoint="endpoint", credential=AzureKeyCredential("key")) +client_a.operations.op_a() +client_a.sub_namespace.sub_op_a() + +# ServiceB client +client_b = ServiceBClient(endpoint="endpoint", credential=AzureKeyCredential("key")) +client_b.operations.op_b() +client_b.sub_namespace.sub_op_b() +``` + ### Scenario 2: Services as Direct Children (No Deep Auto-Merge) In the first step design, when combining multiple services with an empty client namespace, all nested namespaces/interfaces from all services are auto-merged into the root client as children. Some users prefer to keep each service's namespace as a direct child of the root client without deep merging. @@ -415,12 +449,16 @@ clients: - kind: client name: Operations parent: *compute + clientInitialization: + initializedBy: parent methods: - kind: basic name: opA - kind: client name: SubNamespace parent: *compute + clientInitialization: + initializedBy: parent methods: - kind: basic name: subOpA @@ -435,12 +473,16 @@ clients: - kind: client name: Operations parent: *disk + clientInitialization: + initializedBy: parent methods: - kind: basic name: opB - kind: client name: SubNamespace parent: *disk + clientInitialization: + initializedBy: parent methods: - kind: basic name: subOpB @@ -460,18 +502,6 @@ client.disk.operations.op_b() client.disk.sub_namespace.sub_op_b() ``` -Compared to the first step design (empty namespace, auto-merge): - -```python -client = CombineClient(endpoint="endpoint", credential=AzureKeyCredential("key")) - -# ServiceA and ServiceB namespaces are auto-merged at root level -client.operations.op_a() # Both opA and opB are in same merged Operations group -client.operations.op_b() -client.sub_namespace.sub_op_a() -client.sub_namespace.sub_op_b() -``` - ### Scenario 3: Fully Customized Client Hierarchy For maximum flexibility, users can fully customize how operations from different services are organized into client hierarchies. This uses nested `@client` decorators with explicit operation mapping. @@ -527,11 +557,15 @@ clients: kind: client name: CustomClient apiVersions: [] + clientInitialization: + initializedBy: individually children: - kind: client name: SharedOperations parent: *root apiVersions: [] # Empty because operations come from different services with different versioning + clientInitialization: + initializedBy: parent methods: - kind: basic name: opA @@ -541,6 +575,8 @@ clients: name: ServiceAOnly parent: *root apiVersions: [av1, av2] + clientInitialization: + initializedBy: parent methods: - kind: basic name: subOpA @@ -548,6 +584,8 @@ clients: name: ServiceBOnly parent: *root apiVersions: [bv1, bv2] + clientInitialization: + initializedBy: parent methods: - kind: basic name: subOpB @@ -596,7 +634,6 @@ The nested `@client` approach works alongside existing customization decorators: - Each nested `@client` with an empty namespace auto-merges its service's content - Each nested `@client` with explicit operations uses only those operations - Operations not referenced by any explicit client are omitted from the SDK - - A diagnostic warning is issued for unreferenced operations ### Changes Needed @@ -604,6 +641,3 @@ The nested `@client` approach works alongside existing customization decorators: - Check if the client namespace has nested `@client` decorators - When nested `@client` decorators exist, use the explicitly defined hierarchy - When the namespace is empty, auto-merge services' content (existing behavior) - -2. **Add validation**: - - Emit diagnostic if explicit clients have unreferenced operations from the services From b837d38304f57a53b43856b35802954a0f5c056e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 09:28:31 +0000 Subject: [PATCH 08/18] Add 2D apiVersions and endpoint/credential limitations documentation Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> --- .../design-docs/multiple-services.md | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/typespec-client-generator-core/design-docs/multiple-services.md b/packages/typespec-client-generator-core/design-docs/multiple-services.md index 62258563ca..0eeff8a775 100644 --- a/packages/typespec-client-generator-core/design-docs/multiple-services.md +++ b/packages/typespec-client-generator-core/design-docs/multiple-services.md @@ -87,7 +87,7 @@ namespace CombineClient; When TCGC detects multiple services in one client, it will: -1. Create the root client for the combined client. If any service is versioned, the root client's initialization method will have an `apiVersion` parameter with no default value. The `apiVersions` property and the `apiVersion` parameter for the root client will be empty (since multiple services' API versions cannot be combined). The root client's endpoint and credential parameters will be created based on the first sub-service, which means all sub-services must share the same endpoint and credential. +1. Create the root client for the combined client. If any service is versioned, the root client's initialization method will have an `apiVersion` parameter with no default value. The `apiVersions` property for the root client will be a 2-dimensional array to store all possible API versions for each service (e.g., `[[av1, av2], [bv1, bv2]]`). The root client's endpoint and credential parameters will be created based on the first sub-service, which means all sub-services must share the same endpoint and credential. 2. Create sub-clients for each service's nested namespaces or interfaces. Each sub-client will have its own `apiVersion` property and initialization method if the service is versioned. 3. If multiple services have nested namespaces or interfaces with the same name, TCGC will automatically merge them into a single operation group. The merged operation group will have empty `apiVersions` and a `string` type for the API version parameter, and will contain operations from all the services. 4. Operations directly under each service's namespace are placed under the root client. Operations under nested namespaces or interfaces are placed under the corresponding sub-clients. @@ -102,7 +102,7 @@ clients: - &a1 kind: client name: CombineClient - apiVersions: [] + apiVersions: [[av1, av2], [bv1, bv2]] # 2-dimensional array for multiple services clientInitialization: kind: clientinitialization parameters: @@ -112,7 +112,7 @@ clients: onClient: true - kind: method name: apiVersion - apiVersions: [] + apiVersions: [[av1, av2], [bv1, bv2]] clientDefaultValue: undefined isGeneratedName: false onClient: true @@ -391,6 +391,10 @@ client_b.sub_namespace.sub_op_b() In the first step design, when combining multiple services with an empty client namespace, all nested namespaces/interfaces from all services are auto-merged into the root client as children. Some users prefer to keep each service's namespace as a direct child of the root client without deep merging. +#### Endpoint and Credential Limitations + +When combining multiple services into a single client, all services must share the same endpoint and credential configuration. The root client's endpoint and credential parameters are created based on the first service in the array. If services have different `@server` or `@useAuth` definitions, emitters should report a diagnostic error. + #### Syntax Proposal Use nested `@client` decorators to explicitly define each service as a child client: @@ -434,7 +438,7 @@ clients: - &root kind: client name: CombineClient - apiVersions: [] + apiVersions: [[av1, av2], [bv1, bv2]] # 2-dimensional array for multiple services clientInitialization: initializedBy: individually children: @@ -506,6 +510,10 @@ client.disk.sub_namespace.sub_op_b() For maximum flexibility, users can fully customize how operations from different services are organized into client hierarchies. This uses nested `@client` decorators with explicit operation mapping. +#### Endpoint and Credential Limitations + +Same as Scenario 2: when combining multiple services into a single client, all services must share the same endpoint and credential configuration. The root client's endpoint and credential parameters are created based on the first service in the array. + #### Syntax Proposal Use nested `@client` decorators with explicit operation references to create a custom client hierarchy: @@ -556,14 +564,14 @@ clients: - &root kind: client name: CustomClient - apiVersions: [] + apiVersions: [[av1, av2], [bv1, bv2]] # 2-dimensional array for multiple services clientInitialization: initializedBy: individually children: - kind: client name: SharedOperations parent: *root - apiVersions: [] # Empty because operations come from different services with different versioning + apiVersions: [[av1, av2], [bv1, bv2]] # 2-dimensional because operations come from different services clientInitialization: initializedBy: parent methods: From c0e3cd3c567d607c4885e14df8a1e83fee6a9f6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 09:34:10 +0000 Subject: [PATCH 09/18] Expand Changes Needed section with detailed implementation requirements Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> --- .../design-docs/multiple-services.md | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/typespec-client-generator-core/design-docs/multiple-services.md b/packages/typespec-client-generator-core/design-docs/multiple-services.md index 0eeff8a775..81acf3081c 100644 --- a/packages/typespec-client-generator-core/design-docs/multiple-services.md +++ b/packages/typespec-client-generator-core/design-docs/multiple-services.md @@ -645,7 +645,27 @@ The nested `@client` approach works alongside existing customization decorators: ### Changes Needed -1. **Update `cache.ts` logic**: - - Check if the client namespace has nested `@client` decorators +1. **Update `interfaces.ts`**: + - Update `SdkClientType.apiVersions` type from `string[]` to `string[] | string[][]` to support 2-dimensional array for multi-service clients + - This change affects both the root client and child clients that contain operations from multiple services + +2. **Update `cache.ts` logic**: + - In `prepareClientAndOperationCache`: Check if the client namespace has nested `@client` decorators before auto-merging - When nested `@client` decorators exist, use the explicitly defined hierarchy - When the namespace is empty, auto-merge services' content (existing behavior) + - Update `getOrCreateClients`: Handle nested `@client` detection within multi-service clients + +3. **Update `internal-utils.ts`**: + - Modify `hasExplicitClientOrOperationGroup` to properly detect nested `@client` decorators within multi-service client namespaces + - Currently it returns `false` when a client has multiple services, but should return `true` if the namespace contains nested `@client` decorators + +4. **Update `clients.ts`**: + - Update `createSdkClientType` to populate `apiVersions` as a 2-dimensional array when the client spans multiple services + - Update endpoint and credential parameter creation to validate that all services share the same `@server` and `@useAuth` definitions + +5. **Update `decorators.ts`**: + - Add validation in `@client` decorator to ensure services combined into a single client have compatible endpoint and credential configurations + +6. **Add validation diagnostics**: + - Add diagnostic when services combined into a client have different `@server` definitions + - Add diagnostic when services combined into a client have different `@useAuth` definitions From c4d9bce05cf213ea066b1cca6f14ec8ff6b0af57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 09:47:26 +0000 Subject: [PATCH 10/18] Format the design doc with prettier Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> --- .../design-docs/multiple-services.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/typespec-client-generator-core/design-docs/multiple-services.md b/packages/typespec-client-generator-core/design-docs/multiple-services.md index 81acf3081c..a5749d04a1 100644 --- a/packages/typespec-client-generator-core/design-docs/multiple-services.md +++ b/packages/typespec-client-generator-core/design-docs/multiple-services.md @@ -102,7 +102,7 @@ clients: - &a1 kind: client name: CombineClient - apiVersions: [[av1, av2], [bv1, bv2]] # 2-dimensional array for multiple services + apiVersions: [[av1, av2], [bv1, bv2]] # 2-dimensional array for multiple services clientInitialization: kind: clientinitialization parameters: @@ -438,7 +438,7 @@ clients: - &root kind: client name: CombineClient - apiVersions: [[av1, av2], [bv1, bv2]] # 2-dimensional array for multiple services + apiVersions: [[av1, av2], [bv1, bv2]] # 2-dimensional array for multiple services clientInitialization: initializedBy: individually children: @@ -564,14 +564,14 @@ clients: - &root kind: client name: CustomClient - apiVersions: [[av1, av2], [bv1, bv2]] # 2-dimensional array for multiple services + apiVersions: [[av1, av2], [bv1, bv2]] # 2-dimensional array for multiple services clientInitialization: initializedBy: individually children: - kind: client name: SharedOperations parent: *root - apiVersions: [[av1, av2], [bv1, bv2]] # 2-dimensional because operations come from different services + apiVersions: [[av1, av2], [bv1, bv2]] # 2-dimensional because operations come from different services clientInitialization: initializedBy: parent methods: @@ -617,11 +617,11 @@ client.service_b_only.sub_op_b() ### Summary of Client Hierarchy Behavior -| Scenario | Client Namespace Content | Result | -| -------------------------- | ------------------------------------- | ---------------------------------------------------------------------- | -| First step design | Empty | All services' nested items auto-merged as root client's children | -| Services as children | Nested `@client` (empty namespaces) | Each nested client auto-merges its service's content | -| Fully customized | Nested `@client` with explicit ops | Only explicitly defined clients and operations are used | +| Scenario | Client Namespace Content | Result | +| -------------------- | ----------------------------------- | ---------------------------------------------------------------- | +| First step design | Empty | All services' nested items auto-merged as root client's children | +| Services as children | Nested `@client` (empty namespaces) | Each nested client auto-merges its service's content | +| Fully customized | Nested `@client` with explicit ops | Only explicitly defined clients and operations are used | ### Interaction with Existing Decorators From eaab34ae448d1e9dd86b4c7a5d95b2436f2b60dc Mon Sep 17 00:00:00 2001 From: tadelesh Date: Thu, 29 Jan 2026 12:57:30 +0800 Subject: [PATCH 11/18] format --- .../design-docs/multiple-services.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/typespec-client-generator-core/design-docs/multiple-services.md b/packages/typespec-client-generator-core/design-docs/multiple-services.md index a5749d04a1..ffacfe43e1 100644 --- a/packages/typespec-client-generator-core/design-docs/multiple-services.md +++ b/packages/typespec-client-generator-core/design-docs/multiple-services.md @@ -272,7 +272,10 @@ Define multiple `@client` decorators, each targeting one service: @service @versioned(VersionsA) namespace ServiceA { - enum VersionsA { av1, av2 } + enum VersionsA { + av1, + av2, + } interface Operations { opA(): void; @@ -286,7 +289,10 @@ namespace ServiceA { @service @versioned(VersionsB) namespace ServiceB { - enum VersionsB { bv1, bv2 } + enum VersionsB { + bv1, + bv2, + } interface Operations { opB(): void; From c0517f207576600d947fb461a49d7430596b9fd4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 06:45:16 +0000 Subject: [PATCH 12/18] Replace 2D apiVersions array with apiVersionsMap for cross-service clients Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> --- .../design-docs/multiple-services.md | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/typespec-client-generator-core/design-docs/multiple-services.md b/packages/typespec-client-generator-core/design-docs/multiple-services.md index ffacfe43e1..0164eb3d66 100644 --- a/packages/typespec-client-generator-core/design-docs/multiple-services.md +++ b/packages/typespec-client-generator-core/design-docs/multiple-services.md @@ -87,7 +87,7 @@ namespace CombineClient; When TCGC detects multiple services in one client, it will: -1. Create the root client for the combined client. If any service is versioned, the root client's initialization method will have an `apiVersion` parameter with no default value. The `apiVersions` property for the root client will be a 2-dimensional array to store all possible API versions for each service (e.g., `[[av1, av2], [bv1, bv2]]`). The root client's endpoint and credential parameters will be created based on the first sub-service, which means all sub-services must share the same endpoint and credential. +1. Create the root client for the combined client. If any service is versioned, the root client's initialization method will have an `apiVersion` parameter with no default value. For cross-service clients, the `apiVersions` property will be an empty array `[]`, and a new `apiVersionsMap` property will store a map of service namespace full qualified names to their API versions (e.g., `{"ServiceA": ["av1", "av2"], "ServiceB": ["bv1", "bv2"]}`). The root client's endpoint and credential parameters will be created based on the first sub-service, which means all sub-services must share the same endpoint and credential. If services have different `@server` or `@useAuth` definitions, TCGC will report a diagnostic error. 2. Create sub-clients for each service's nested namespaces or interfaces. Each sub-client will have its own `apiVersion` property and initialization method if the service is versioned. 3. If multiple services have nested namespaces or interfaces with the same name, TCGC will automatically merge them into a single operation group. The merged operation group will have empty `apiVersions` and a `string` type for the API version parameter, and will contain operations from all the services. 4. Operations directly under each service's namespace are placed under the root client. Operations under nested namespaces or interfaces are placed under the corresponding sub-clients. @@ -102,7 +102,10 @@ clients: - &a1 kind: client name: CombineClient - apiVersions: [[av1, av2], [bv1, bv2]] # 2-dimensional array for multiple services + apiVersions: [] # Empty for cross-service clients + apiVersionsMap: # Map of service namespace to API versions + ServiceA: [av1, av2] + ServiceB: [bv1, bv2] clientInitialization: kind: clientinitialization parameters: @@ -112,7 +115,10 @@ clients: onClient: true - kind: method name: apiVersion - apiVersions: [[av1, av2], [bv1, bv2]] + apiVersions: [] + apiVersionsMap: + ServiceA: [av1, av2] + ServiceB: [bv1, bv2] clientDefaultValue: undefined isGeneratedName: false onClient: true @@ -444,7 +450,10 @@ clients: - &root kind: client name: CombineClient - apiVersions: [[av1, av2], [bv1, bv2]] # 2-dimensional array for multiple services + apiVersions: [] # Empty for cross-service clients + apiVersionsMap: # Map of service namespace to API versions + ServiceA: [av1, av2] + ServiceB: [bv1, bv2] clientInitialization: initializedBy: individually children: @@ -570,14 +579,20 @@ clients: - &root kind: client name: CustomClient - apiVersions: [[av1, av2], [bv1, bv2]] # 2-dimensional array for multiple services + apiVersions: [] # Empty for cross-service clients + apiVersionsMap: # Map of service namespace to API versions + ServiceA: [av1, av2] + ServiceB: [bv1, bv2] clientInitialization: initializedBy: individually children: - kind: client name: SharedOperations parent: *root - apiVersions: [[av1, av2], [bv1, bv2]] # 2-dimensional because operations come from different services + apiVersions: [] # Empty because operations come from different services + apiVersionsMap: # Map because operations come from different services + ServiceA: [av1, av2] + ServiceB: [bv1, bv2] clientInitialization: initializedBy: parent methods: @@ -652,8 +667,9 @@ The nested `@client` approach works alongside existing customization decorators: ### Changes Needed 1. **Update `interfaces.ts`**: - - Update `SdkClientType.apiVersions` type from `string[]` to `string[] | string[][]` to support 2-dimensional array for multi-service clients - - This change affects both the root client and child clients that contain operations from multiple services + - Add new `apiVersionsMap` property to `SdkClientType` with type `Record` (key is service namespace full qualified name, value is API versions array) + - Keep existing `apiVersions` property as `string[]` for backward compatibility + - For cross-service clients, `apiVersions` will be empty `[]` and `apiVersionsMap` will contain the mapping 2. **Update `cache.ts` logic**: - In `prepareClientAndOperationCache`: Check if the client namespace has nested `@client` decorators before auto-merging @@ -666,7 +682,8 @@ The nested `@client` approach works alongside existing customization decorators: - Currently it returns `false` when a client has multiple services, but should return `true` if the namespace contains nested `@client` decorators 4. **Update `clients.ts`**: - - Update `createSdkClientType` to populate `apiVersions` as a 2-dimensional array when the client spans multiple services + - Update `createSdkClientType` to populate `apiVersionsMap` when the client spans multiple services + - Keep `apiVersions` as empty array for cross-service clients - Update endpoint and credential parameter creation to validate that all services share the same `@server` and `@useAuth` definitions 5. **Update `decorators.ts`**: From 448799a76935166f7eb5f97bfeda561e4fee3f9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 06:49:16 +0000 Subject: [PATCH 13/18] Update design: multiple @service namespaces create separate root clients by default Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> --- .../design-docs/client.md | 2 +- .../design-docs/multiple-services.md | 72 +++++++++++++------ 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/packages/typespec-client-generator-core/design-docs/client.md b/packages/typespec-client-generator-core/design-docs/client.md index bd88e7198d..8c55354db2 100644 --- a/packages/typespec-client-generator-core/design-docs/client.md +++ b/packages/typespec-client-generator-core/design-docs/client.md @@ -71,7 +71,7 @@ sub_client.do_something() The entrance of TCGC is `SdkPackage` which represents a complete package and includes clients, models, etc. The clients depend on the combination usage of namespace, interface, `@service`, `@client`. -If there is no explicitly defined `@client`, then the first namespaces with `@service` will be a client. The nested namespaces and interfaces under that namespace will be a sub client with hierarchy. +If there is no explicitly defined `@client`, then each namespace with `@service` will be a separate root client. The nested namespaces and interfaces under each service namespace will be sub clients with hierarchy. - Example 1: diff --git a/packages/typespec-client-generator-core/design-docs/multiple-services.md b/packages/typespec-client-generator-core/design-docs/multiple-services.md index 0164eb3d66..876fc1ef4d 100644 --- a/packages/typespec-client-generator-core/design-docs/multiple-services.md +++ b/packages/typespec-client-generator-core/design-docs/multiple-services.md @@ -259,20 +259,52 @@ The resulting `SharedGroup` operation group will have: ## Extended Design: Advanced Client Hierarchy Customization -The first step design focuses on automatically merging multiple services into one client when the client namespace is empty. However, users have requested more flexibility in how they organize clients from multiple services. This section extends the previous [client hierarchy design](./client.md) to provide additional scenarios by leveraging nested `@client` decorators. +The first step design focuses on explicitly merging multiple services into one client using `@client` with an array of services. This section extends the previous [client hierarchy design](./client.md) to clarify the behavior when no explicit `@client` is defined and to provide additional scenarios. -The key design principle is: +### Default Behavior: Multiple Services Without Explicit `@client` -- **If the client namespace is empty**: TCGC auto-merges all services' nested namespaces/interfaces into the current client as children (first step design behavior). -- **If the client namespace contains nested `@client` decorators**: TCGC uses the explicitly defined client hierarchy instead of auto-merging. +When there are multiple `@service` namespaces and no explicit `@client` decorator is defined, TCGC will automatically create a separate root client for each `@service` namespace. This matches the single-service behavior where each `@service` namespace becomes its own client. -### Scenario 1: Multiple Clients, Each Belonging to One Service +For example, given two services without explicit `@client`: -In some cases, users may want to generate separate clients for each service rather than combining them into one client. This is useful when services have different authentication, versioning, or other client-level settings. +```typespec title="main.tsp" +@service +@versioned(VersionsA) +namespace ServiceA { + enum VersionsA { av1, av2 } + + interface Operations { + opA(): void; + } +} + +@service +@versioned(VersionsB) +namespace ServiceB { + enum VersionsB { bv1, bv2 } + + interface Operations { + opB(): void; + } +} +``` + +TCGC will automatically generate two root clients: `ServiceAClient` and `ServiceBClient`, each with their own API versions and children. + +### Explicit Client Definition Scenarios + +When explicit `@client` decorators are used, TCGC follows the explicitly defined client hierarchy. The key design principle is: + +- **If the `@client` namespace is empty**: TCGC auto-merges all services' nested namespaces/interfaces into the current client as children. +- **If the `@client` namespace contains nested `@client` decorators**: TCGC uses the explicitly defined client hierarchy instead of auto-merging. + +### Scenario 1: Explicit Client Names for Multiple Services + +Users may want to explicitly define clients for each service with custom names. Without explicit `@client`, TCGC would use the service namespace name with a `Client` suffix (e.g., `ServiceAClient`). With explicit `@client`, users can customize the client name. #### Syntax Proposal -Define multiple `@client` decorators, each targeting one service: +Define multiple `@client` decorators, each targeting one service with a custom name: ```typespec title="main.tsp" @service @@ -317,21 +349,21 @@ import "@azure-tools/typespec-client-generator-core"; using Azure.ClientGenerator.Core; @client({ - name: "ServiceAClient", + name: "MyServiceAClient", // Custom name instead of default "ServiceAClient" service: ServiceA, }) -namespace ServiceAClient; +namespace MyServiceAClient; @client({ - name: "ServiceBClient", + name: "MyServiceBClient", // Custom name instead of default "ServiceBClient" service: ServiceB, }) -namespace ServiceBClient; +namespace MyServiceBClient; ``` #### TCGC Behavior -This creates two independent root clients, each with their own service hierarchy: +This creates two independent root clients with custom names, each with their own service hierarchy: According to [client.md](./client.md), the default value of `initializedBy` for a root client is `InitializedBy.individually`, while for a sub client it is `InitializedBy.parent`. @@ -339,7 +371,7 @@ According to [client.md](./client.md), the default value of `initializedBy` for clients: - &a1 kind: client - name: ServiceAClient + name: MyServiceAClient apiVersions: [av1, av2] clientInitialization: initializedBy: individually @@ -362,7 +394,7 @@ clients: name: subOpA - &a2 kind: client - name: ServiceBClient + name: MyServiceBClient apiVersions: [bv1, bv2] clientInitialization: initializedBy: individually @@ -388,13 +420,13 @@ clients: #### Python SDK Example ```python -# ServiceA client -client_a = ServiceAClient(endpoint="endpoint", credential=AzureKeyCredential("key")) +# ServiceA client with custom name +client_a = MyServiceAClient(endpoint="endpoint", credential=AzureKeyCredential("key")) client_a.operations.op_a() client_a.sub_namespace.sub_op_a() -# ServiceB client -client_b = ServiceBClient(endpoint="endpoint", credential=AzureKeyCredential("key")) +# ServiceB client with custom name +client_b = MyServiceBClient(endpoint="endpoint", credential=AzureKeyCredential("key")) client_b.operations.op_b() client_b.sub_namespace.sub_op_b() ``` @@ -672,10 +704,10 @@ The nested `@client` approach works alongside existing customization decorators: - For cross-service clients, `apiVersions` will be empty `[]` and `apiVersionsMap` will contain the mapping 2. **Update `cache.ts` logic**: + - In `getOrCreateClients`: When no explicit `@client` is defined, create a separate root client for each `@service` namespace (not just the first one) - In `prepareClientAndOperationCache`: Check if the client namespace has nested `@client` decorators before auto-merging - When nested `@client` decorators exist, use the explicitly defined hierarchy - - When the namespace is empty, auto-merge services' content (existing behavior) - - Update `getOrCreateClients`: Handle nested `@client` detection within multi-service clients + - When the namespace is empty, auto-merge services' content (existing behavior for explicit multi-service clients) 3. **Update `internal-utils.ts`**: - Modify `hasExplicitClientOrOperationGroup` to properly detect nested `@client` decorators within multi-service client namespaces From 59382d34a3a3ee55873e2c48c1b71de6cdec0de3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 06:56:35 +0000 Subject: [PATCH 14/18] Add Scenario 0 for default multiple services behavior with full YAML and Python examples Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> --- .../design-docs/multiple-services.md | 84 ++++++++++++++++++- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/packages/typespec-client-generator-core/design-docs/multiple-services.md b/packages/typespec-client-generator-core/design-docs/multiple-services.md index 876fc1ef4d..a8a6bdb865 100644 --- a/packages/typespec-client-generator-core/design-docs/multiple-services.md +++ b/packages/typespec-client-generator-core/design-docs/multiple-services.md @@ -261,11 +261,13 @@ The resulting `SharedGroup` operation group will have: The first step design focuses on explicitly merging multiple services into one client using `@client` with an array of services. This section extends the previous [client hierarchy design](./client.md) to clarify the behavior when no explicit `@client` is defined and to provide additional scenarios. -### Default Behavior: Multiple Services Without Explicit `@client` +### Scenario 0: Multiple Services Without Explicit `@client` (Default Behavior) When there are multiple `@service` namespaces and no explicit `@client` decorator is defined, TCGC will automatically create a separate root client for each `@service` namespace. This matches the single-service behavior where each `@service` namespace becomes its own client. -For example, given two services without explicit `@client`: +#### Syntax Proposal + +No `client.tsp` file is needed. Each `@service` namespace automatically becomes a root client: ```typespec title="main.tsp" @service @@ -276,6 +278,10 @@ namespace ServiceA { interface Operations { opA(): void; } + + namespace SubNamespace { + op subOpA(): void; + } } @service @@ -286,10 +292,82 @@ namespace ServiceB { interface Operations { opB(): void; } + + namespace SubNamespace { + op subOpB(): void; + } } ``` -TCGC will automatically generate two root clients: `ServiceAClient` and `ServiceBClient`, each with their own API versions and children. +#### TCGC Behavior + +TCGC will automatically generate two independent root clients, each with their own API versions and children: + +According to [client.md](./client.md), the default value of `initializedBy` for a root client is `InitializedBy.individually`, while for a sub client it is `InitializedBy.parent`. + +```yaml +clients: + - &a1 + kind: client + name: ServiceAClient + apiVersions: [av1, av2] + clientInitialization: + initializedBy: individually + children: + - kind: client + name: Operations + parent: *a1 + clientInitialization: + initializedBy: parent + methods: + - kind: basic + name: opA + - kind: client + name: SubNamespace + parent: *a1 + clientInitialization: + initializedBy: parent + methods: + - kind: basic + name: subOpA + - &a2 + kind: client + name: ServiceBClient + apiVersions: [bv1, bv2] + clientInitialization: + initializedBy: individually + children: + - kind: client + name: Operations + parent: *a2 + clientInitialization: + initializedBy: parent + methods: + - kind: basic + name: opB + - kind: client + name: SubNamespace + parent: *a2 + clientInitialization: + initializedBy: parent + methods: + - kind: basic + name: subOpB +``` + +#### Python SDK Example + +```python +# ServiceA client (auto-generated name) +client_a = ServiceAClient(endpoint="endpoint", credential=AzureKeyCredential("key")) +client_a.operations.op_a() +client_a.sub_namespace.sub_op_a() + +# ServiceB client (auto-generated name) +client_b = ServiceBClient(endpoint="endpoint", credential=AzureKeyCredential("key")) +client_b.operations.op_b() +client_b.sub_namespace.sub_op_b() +``` ### Explicit Client Definition Scenarios From e613f9b6860ab41bf325382d0a2b29573d29f92d Mon Sep 17 00:00:00 2001 From: tadelesh Date: Fri, 30 Jan 2026 16:10:33 +0800 Subject: [PATCH 15/18] format --- .../design-docs/multiple-services.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/typespec-client-generator-core/design-docs/multiple-services.md b/packages/typespec-client-generator-core/design-docs/multiple-services.md index a8a6bdb865..e176ccfa87 100644 --- a/packages/typespec-client-generator-core/design-docs/multiple-services.md +++ b/packages/typespec-client-generator-core/design-docs/multiple-services.md @@ -273,7 +273,10 @@ No `client.tsp` file is needed. Each `@service` namespace automatically becomes @service @versioned(VersionsA) namespace ServiceA { - enum VersionsA { av1, av2 } + enum VersionsA { + av1, + av2, + } interface Operations { opA(): void; @@ -287,7 +290,10 @@ namespace ServiceA { @service @versioned(VersionsB) namespace ServiceB { - enum VersionsB { bv1, bv2 } + enum VersionsB { + bv1, + bv2, + } interface Operations { opB(): void; From af1d37825f3598cbff4b5a6f50fd152835aa119b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 05:23:13 +0000 Subject: [PATCH 16/18] Add Scenario 1.5 for mixing multi-service and single-service clients Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> --- .../design-docs/multiple-services.md | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/packages/typespec-client-generator-core/design-docs/multiple-services.md b/packages/typespec-client-generator-core/design-docs/multiple-services.md index e176ccfa87..fc0fc4c71c 100644 --- a/packages/typespec-client-generator-core/design-docs/multiple-services.md +++ b/packages/typespec-client-generator-core/design-docs/multiple-services.md @@ -515,6 +515,132 @@ client_b.operations.op_b() client_b.sub_namespace.sub_op_b() ``` +### Scenario 1.5: Mixing Multi-Service and Single-Service Clients + +Users may want to combine multiple services into one client while keeping another service as a separate client. This scenario shows how to mix multi-service clients with single-service clients. + +#### Syntax Proposal + +Define clients where one client targets multiple services and another targets a single service: + +```typespec title="main.tsp" +@service +@versioned(VersionsA) +namespace ServiceA { + enum VersionsA { av1, av2 } + + interface Operations { + opA(): void; + } +} + +@service +@versioned(VersionsB) +namespace ServiceB { + enum VersionsB { bv1, bv2 } + + interface Operations { + opB(): void; + } +} + +@service +@versioned(VersionsC) +namespace ServiceC { + enum VersionsC { cv1, cv2 } + + interface Operations { + opC(): void; + } +} +``` + +```typespec title="client.tsp" +import "./main.tsp"; +import "@azure-tools/typespec-client-generator-core"; + +using Azure.ClientGenerator.Core; + +// Multi-service client combining ServiceA and ServiceB +@client({ + name: "CombinedABClient", + service: [ServiceA, ServiceB], +}) +namespace CombinedABClient; + +// Single-service client for ServiceC +@client({ + name: "ServiceCClient", + service: ServiceC, +}) +namespace ServiceCClient; +``` + +#### TCGC Behavior + +This creates two root clients: + +1. `CombinedABClient`: A multi-service client that auto-merges ServiceA and ServiceB content (since the namespace is empty) +2. `ServiceCClient`: A single-service client for ServiceC + +According to [client.md](./client.md), the default value of `initializedBy` for a root client is `InitializedBy.individually`, while for a sub client it is `InitializedBy.parent`. + +```yaml +clients: + - &combined + kind: client + name: CombinedABClient + apiVersions: [] # Empty for cross-service clients + apiVersionsMap: + ServiceA: [av1, av2] + ServiceB: [bv1, bv2] + clientInitialization: + initializedBy: individually + children: + - kind: client + name: Operations # Merged from both ServiceA and ServiceB + parent: *combined + apiVersions: [] # Empty because operations come from different services + apiVersionsMap: + ServiceA: [av1, av2] + ServiceB: [bv1, bv2] + clientInitialization: + initializedBy: parent + methods: + - kind: basic + name: opA + - kind: basic + name: opB + - &serviceC + kind: client + name: ServiceCClient + apiVersions: [cv1, cv2] + clientInitialization: + initializedBy: individually + children: + - kind: client + name: Operations + parent: *serviceC + clientInitialization: + initializedBy: parent + methods: + - kind: basic + name: opC +``` + +#### Python SDK Example + +```python +# Combined client for ServiceA and ServiceB +combined_client = CombinedABClient(endpoint="endpoint", credential=AzureKeyCredential("key")) +combined_client.operations.op_a() # From ServiceA +combined_client.operations.op_b() # From ServiceB + +# Separate client for ServiceC +service_c_client = ServiceCClient(endpoint="endpoint", credential=AzureKeyCredential("key")) +service_c_client.operations.op_c() +``` + ### Scenario 2: Services as Direct Children (No Deep Auto-Merge) In the first step design, when combining multiple services with an empty client namespace, all nested namespaces/interfaces from all services are auto-merged into the root client as children. Some users prefer to keep each service's namespace as a direct child of the root client without deep merging. From 1c7e6f2299d2086ca9f32d5adc7d10fc91b1a635 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 05:42:32 +0000 Subject: [PATCH 17/18] Clarify that only services in the service array are included in client Co-authored-by: JialinHuang803 <139532647+JialinHuang803@users.noreply.github.com> --- .../design-docs/multiple-services.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/typespec-client-generator-core/design-docs/multiple-services.md b/packages/typespec-client-generator-core/design-docs/multiple-services.md index fc0fc4c71c..c2d100f8d7 100644 --- a/packages/typespec-client-generator-core/design-docs/multiple-services.md +++ b/packages/typespec-client-generator-core/design-docs/multiple-services.md @@ -897,8 +897,9 @@ The nested `@client` approach works alongside existing customization decorators: ### Validation Rules 1. When the root client namespace is empty: - - All services' content is auto-merged into the root client - - Same-named namespaces/interfaces across services are merged together + - Only services listed in the `service` array of the `@client` decorator are included in the client + - Content from these listed services is auto-merged into the root client + - Same-named namespaces/interfaces across the listed services are merged together 2. When the root client namespace has nested `@client` decorators: - Only explicitly defined clients are created at the root level From ce258e013e1ef84d63c07e05d5dcd1a3c8895046 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 09:08:21 +0000 Subject: [PATCH 18/18] Add service property to all nested @client decorators in Scenario 3 Co-authored-by: tadelesh <1726438+tadelesh@users.noreply.github.com> --- .../design-docs/multiple-services.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/typespec-client-generator-core/design-docs/multiple-services.md b/packages/typespec-client-generator-core/design-docs/multiple-services.md index c2d100f8d7..66adcf2294 100644 --- a/packages/typespec-client-generator-core/design-docs/multiple-services.md +++ b/packages/typespec-client-generator-core/design-docs/multiple-services.md @@ -787,20 +787,29 @@ using Azure.ClientGenerator.Core; }) namespace CustomClient { // Custom child client combining operations from both services - @client + @client({ + name: "SharedOperations", + service: [ServiceA, ServiceB], + }) interface SharedOperations { opA is ServiceA.Operations.opA; opB is ServiceB.Operations.opB; } // Custom child client with operations from ServiceA only - @client + @client({ + name: "ServiceAOnly", + service: ServiceA, + }) interface ServiceAOnly { subOpA is ServiceA.SubNamespace.subOpA; } // Custom child client with operations from ServiceB only - @client + @client({ + name: "ServiceBOnly", + service: ServiceB, + }) interface ServiceBOnly { subOpB is ServiceB.SubNamespace.subOpB; }