JsonElementBackedEvents => new()
{
{
diff --git a/go/client_test.go b/go/client_test.go
index b701de437f..f8ff896d69 100644
--- a/go/client_test.go
+++ b/go/client_test.go
@@ -407,42 +407,63 @@ func newRuntimeShutdownRpcPair(t *testing.T) (*jsonrpc2.Client, *jsonrpc2.Client
}
func TestClient_ForwardsCapiOptionsToSessionRequests(t *testing.T) {
- rpcClient, server, _ := newRuntimeShutdownRpcPair(t)
- t.Cleanup(server.Stop)
- client := &Client{
- client: rpcClient,
- RPC: rpc.NewServerRPC(rpcClient),
- sessions: make(map[string]*Session),
- }
+ tests := []struct {
+ name string
+ capi *CapiSessionOptions
+ want map[string]any
+ }{
+ {"omitted", nil, nil},
+ {"empty", &CapiSessionOptions{}, map[string]any{}},
+ {"websocket only", &CapiSessionOptions{EnableWebSocketResponses: Bool(false)}, map[string]any{"enableWebSocketResponses": false}},
+ {"efficiency", &CapiSessionOptions{AutoTier: AutoTierEfficiency}, map[string]any{"autoTier": "efficiency"}},
+ {"balance", &CapiSessionOptions{AutoTier: AutoTierBalance}, map[string]any{"autoTier": "balance"}},
+ {"intelligence", &CapiSessionOptions{AutoTier: AutoTierIntelligence}, map[string]any{"autoTier": "intelligence"}},
+ {"efficiency with websocket", &CapiSessionOptions{AutoTier: AutoTierEfficiency, EnableWebSocketResponses: Bool(false)}, map[string]any{"autoTier": "efficiency", "enableWebSocketResponses": false}},
+ {"balance with websocket", &CapiSessionOptions{AutoTier: AutoTierBalance, EnableWebSocketResponses: Bool(false)}, map[string]any{"autoTier": "balance", "enableWebSocketResponses": false}},
+ {"intelligence with websocket", &CapiSessionOptions{AutoTier: AutoTierIntelligence, EnableWebSocketResponses: Bool(false)}, map[string]any{"autoTier": "intelligence", "enableWebSocketResponses": false}},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ rpcClient, server, _ := newRuntimeShutdownRpcPair(t)
+ t.Cleanup(server.Stop)
+ client := &Client{
+ client: rpcClient,
+ RPC: rpc.NewServerRPC(rpcClient),
+ sessions: make(map[string]*Session),
+ }
- createParams := make(chan json.RawMessage, 1)
- server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) {
- createParams <- append(json.RawMessage(nil), params...)
- sessionID := sessionIDFromParams(t, params)
- return []byte(`{"sessionId":"` + sessionID + `","workspacePath":"/workspace"}`), nil
- })
+ createParams := make(chan json.RawMessage, 1)
+ server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) {
+ createParams <- append(json.RawMessage(nil), params...)
+ sessionID := sessionIDFromParams(t, params)
+ return []byte(`{"sessionId":"` + sessionID + `","workspacePath":"/workspace"}`), nil
+ })
- _, err := client.CreateSession(t.Context(), &SessionConfig{
- Capi: &CapiSessionOptions{EnableWebSocketResponses: Bool(false)},
- })
- if err != nil {
- t.Fatalf("CreateSession failed: %v", err)
- }
- assertCapiEnableWebSocketResponses(t, <-createParams)
+ _, err := client.CreateSession(t.Context(), &SessionConfig{
+ Model: "auto",
+ Capi: tt.capi,
+ })
+ if err != nil {
+ t.Fatalf("CreateSession failed: %v", err)
+ }
+ assertCapiOptions(t, <-createParams, tt.want)
- resumeParams := make(chan json.RawMessage, 1)
- server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) {
- resumeParams <- append(json.RawMessage(nil), params...)
- return []byte(`{"sessionId":"resumed-capi","workspacePath":"/workspace"}`), nil
- })
+ resumeParams := make(chan json.RawMessage, 1)
+ server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) {
+ resumeParams <- append(json.RawMessage(nil), params...)
+ return []byte(`{"sessionId":"resumed-capi","workspacePath":"/workspace"}`), nil
+ })
- _, err = client.ResumeSessionWithOptions(t.Context(), "resumed-capi", &ResumeSessionConfig{
- Capi: &CapiSessionOptions{EnableWebSocketResponses: Bool(false)},
- })
- if err != nil {
- t.Fatalf("ResumeSessionWithOptions failed: %v", err)
+ _, err = client.ResumeSessionWithOptions(t.Context(), "resumed-capi", &ResumeSessionConfig{
+ Model: "auto",
+ Capi: tt.capi,
+ })
+ if err != nil {
+ t.Fatalf("ResumeSessionWithOptions failed: %v", err)
+ }
+ assertCapiOptions(t, <-resumeParams, tt.want)
+ })
}
- assertCapiEnableWebSocketResponses(t, <-resumeParams)
}
func TestClient_ForwardsAskUserVariantToSessionRequests(t *testing.T) {
@@ -699,7 +720,7 @@ func TestClient_ForwardsNewSessionOptionsToSessionRequests(t *testing.T) {
assertNewSessionOptions(t, <-resumeParams, false, false, "task", 15)
}
-func assertCapiEnableWebSocketResponses(t *testing.T, params json.RawMessage) {
+func assertCapiOptions(t *testing.T, params json.RawMessage, want map[string]any) {
t.Helper()
var decoded map[string]any
@@ -707,12 +728,18 @@ func assertCapiEnableWebSocketResponses(t *testing.T, params json.RawMessage) {
t.Fatalf("failed to unmarshal request params: %v", err)
}
+ if want == nil {
+ if _, present := decoded["capi"]; present {
+ t.Fatalf("expected capi to be omitted, got %v", decoded["capi"])
+ }
+ return
+ }
capi, ok := decoded["capi"].(map[string]any)
if !ok {
t.Fatalf("expected capi object in request params, got %T", decoded["capi"])
}
- if capi["enableWebSocketResponses"] != false {
- t.Fatalf("expected capi.enableWebSocketResponses=false, got %v", capi["enableWebSocketResponses"])
+ if !reflect.DeepEqual(capi, want) {
+ t.Fatalf("expected capi %v, got %v", want, capi)
}
}
diff --git a/go/session_event_serialization_test.go b/go/session_event_serialization_test.go
index ee9258b225..96bf53bb5e 100644
--- a/go/session_event_serialization_test.go
+++ b/go/session_event_serialization_test.go
@@ -14,6 +14,50 @@ var _ SessionEventData = (*rpc.UserMessageData)(nil)
var _ rpc.EmbeddedTextResourceContents = EmbeddedTextResourceContents{}
var _ EmbeddedTextResourceContents = rpc.EmbeddedTextResourceContents{}
+func TestSessionEventAutoTier(t *testing.T) {
+ for _, eventType := range []string{"session.start", "session.resume"} {
+ for _, tier := range []AutoTier{"", AutoTierEfficiency, AutoTierBalance, AutoTierIntelligence} {
+ t.Run(eventType+"/"+string(tier), func(t *testing.T) {
+ data := map[string]any{
+ "sessionId": "test-session", "version": 1,
+ "producer": "copilot", "copilotVersion": "1.0.82-1",
+ "startTime": "2026-08-28T00:00:00Z",
+ "resumeTime": "2026-08-28T00:00:00Z", "eventCount": 1,
+ }
+ if tier != "" {
+ data["autoTier"] = tier
+ }
+ wire, err := json.Marshal(map[string]any{
+ "id": "00000000-0000-0000-0000-000000000001",
+ "timestamp": "2026-08-28T00:00:00Z", "parentId": nil,
+ "type": eventType, "data": data,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ var event SessionEvent
+ if err := json.Unmarshal(wire, &event); err != nil {
+ t.Fatal(err)
+ }
+ var actual *AutoTier
+ switch eventType {
+ case "session.start":
+ actual = event.Data.(*SessionStartData).AutoTier
+ case "session.resume":
+ actual = event.Data.(*SessionResumeData).AutoTier
+ }
+ if tier == "" {
+ if actual != nil {
+ t.Fatalf("expected omitted autoTier, got %v", *actual)
+ }
+ } else if actual == nil || *actual != tier {
+ t.Fatalf("expected autoTier %q, got %v", tier, actual)
+ }
+ })
+ }
+ }
+}
+
func TestSessionEventAgentIDRoundTripsKnownEvent(t *testing.T) {
var event SessionEvent
if err := json.Unmarshal([]byte(`{
diff --git a/go/types.go b/go/types.go
index a574168aba..475fc94a81 100644
--- a/go/types.go
+++ b/go/types.go
@@ -2225,6 +2225,18 @@ func (p ProviderConfig) MarshalJSON() ([]byte, error) {
return json.Marshal(aux)
}
+// AutoTier selects the routing tier for model "auto" with V2 Auto.
+type AutoTier = rpc.AutoTier
+
+const (
+ // AutoTierEfficiency selects the efficiency routing tier.
+ AutoTierEfficiency = rpc.AutoTierEfficiency
+ // AutoTierBalance selects the balance routing tier.
+ AutoTierBalance = rpc.AutoTierBalance
+ // AutoTierIntelligence selects the intelligence routing tier.
+ AutoTierIntelligence = rpc.AutoTierIntelligence
+)
+
// CapiSessionOptions configures provider-scoped Copilot API (CAPI) session behavior.
//
// WebSocket transport is the default for the CAPI Responses API whenever the
@@ -2239,6 +2251,14 @@ type CapiSessionOptions struct {
// WebSocket transport. Enabled by default when the model advertises
// ws:/responses support; set to Bool(false) to force HTTP Responses transport.
EnableWebSocketResponses *bool `json:"enableWebSocketResponses,omitempty"`
+
+ // AutoTier selects the routing tier for model "auto" with V2 Auto.
+ // Requires a runtime that supports Auto tiers; it has no effect outside V2 Auto.
+ // When unset, the runtime uses its default on create and preserves the
+ // persisted or current tier on resume. An explicit tier overrides the
+ // persisted tier on a cold resume; a conflicting tier on a resident
+ // session resume is rejected by the runtime.
+ AutoTier AutoTier `json:"autoTier,omitempty"`
}
// AzureProviderOptions contains Azure-specific provider configuration
diff --git a/go/zsession_events.go b/go/zsession_events.go
index 8731009064..bdd9c918f8 100644
--- a/go/zsession_events.go
+++ b/go/zsession_events.go
@@ -71,7 +71,6 @@ type (
AutoModeSwitchResponse = rpc.AutoModeSwitchResponse
AutopilotObjectiveChangedOperation = rpc.AutopilotObjectiveChangedOperation
AutopilotObjectiveChangedStatus = rpc.AutopilotObjectiveChangedStatus
- AutoTier = rpc.AutoTier
BinaryAssetReference = rpc.BinaryAssetReference
BinaryAssetReferenceType = rpc.BinaryAssetReferenceType
BinaryAssetType = rpc.BinaryAssetType
@@ -476,9 +475,6 @@ const (
AutopilotObjectiveChangedStatusCapReached = rpc.AutopilotObjectiveChangedStatusCapReached
AutopilotObjectiveChangedStatusCompleted = rpc.AutopilotObjectiveChangedStatusCompleted
AutopilotObjectiveChangedStatusPaused = rpc.AutopilotObjectiveChangedStatusPaused
- AutoTierBalance = rpc.AutoTierBalance
- AutoTierEfficiency = rpc.AutoTierEfficiency
- AutoTierIntelligence = rpc.AutoTierIntelligence
BinaryAssetReferenceTypeImage = rpc.BinaryAssetReferenceTypeImage
BinaryAssetReferenceTypeResource = rpc.BinaryAssetReferenceTypeResource
BinaryAssetTypeImage = rpc.BinaryAssetTypeImage
diff --git a/java/README.md b/java/README.md
index e64964255b..18e7bdda8f 100644
--- a/java/README.md
+++ b/java/README.md
@@ -335,6 +335,32 @@ Chain fluent modifiers to set tool options:
For design context and decision rationale, see [ADR-006](docs/adr/adr-006-tool-definition-inline.md).
+## Auto routing tiers
+
+Use `CapiSessionOptions.setAutoTier(...)` to select `AutoTier.EFFICIENCY`,
+`AutoTier.BALANCE`, or `AutoTier.INTELLIGENCE`. This option is meaningful only
+with model `auto` (Auto mode V2).
+It requires a runtime version that supports `capi.autoTier`.
+
+```java
+import com.github.copilot.rpc.AutoTier;
+import com.github.copilot.rpc.CapiSessionOptions;
+import com.github.copilot.rpc.SessionConfig;
+
+var config = new SessionConfig()
+ .setModel("auto")
+ .setCapi(new CapiSessionOptions().setAutoTier(AutoTier.BALANCE));
+```
+
+The same options work with `ResumeSessionConfig.setCapi(...)` and can be combined
+with `setEnableWebSocketResponses(false)`. The SDK omits an unset (`null`) tier:
+the runtime chooses its default on create and preserves the persisted/current
+tier on resume. An explicit tier overrides the persisted tier on cold resume;
+the runtime rejects a conflicting tier when the session is already resident
+in memory. The SDK does not choose a default or manage tier persistence.
+See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence)
+for the lifecycle rules.
+
## Session Store
`enableSessionStore` on `SessionConfig` enables the cross-session store for search and retrieval across sessions. When unset in the default `CopilotClientMode.COPILOT_CLI` mode, the runtime default applies (enabled). In `CopilotClientMode.EMPTY` mode, defaults to disabled.
diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/AutoTier.java b/java/sdk/src/main/java/com/github/copilot/rpc/AutoTier.java
new file mode 100644
index 0000000000..f9117abfb2
--- /dev/null
+++ b/java/sdk/src/main/java/com/github/copilot/rpc/AutoTier.java
@@ -0,0 +1,63 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.rpc;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * Routing tier for the {@code auto} model with Auto mode V2.
+ *
+ * @see CapiSessionOptions#setAutoTier(AutoTier)
+ */
+public enum AutoTier {
+
+ /** Prioritize efficiency. */
+ EFFICIENCY("efficiency"),
+
+ /** Balance efficiency and intelligence. */
+ BALANCE("balance"),
+
+ /** Prioritize intelligence. */
+ INTELLIGENCE("intelligence");
+
+ private final String value;
+
+ AutoTier(String value) {
+ this.value = value;
+ }
+
+ /**
+ * Returns the JSON value for this routing tier.
+ *
+ * @return the string value used in JSON serialization
+ */
+ @JsonValue
+ public String getValue() {
+ return value;
+ }
+
+ /**
+ * Deserializes a JSON string into its routing tier.
+ *
+ * @param value
+ * the JSON string value
+ * @return the matching tier, or {@code null} if value is {@code null}
+ * @throws IllegalArgumentException
+ * if the value does not match a known routing tier
+ */
+ @JsonCreator
+ public static AutoTier fromValue(String value) {
+ if (value == null) {
+ return null;
+ }
+ for (AutoTier tier : values()) {
+ if (tier.value.equals(value)) {
+ return tier;
+ }
+ }
+ throw new IllegalArgumentException("Unknown AutoTier value: " + value);
+ }
+}
diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java b/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java
index d94d59f67b..e401762302 100644
--- a/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java
+++ b/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java
@@ -29,9 +29,40 @@
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CapiSessionOptions {
+ @JsonProperty("autoTier")
+ private AutoTier autoTier;
+
@JsonProperty("enableWebSocketResponses")
private Boolean enableWebSocketResponses;
+ /**
+ * Gets the routing tier for the {@code auto} model (Auto mode V2).
+ *
+ * @return the explicit tier, or {@code null} to leave tier selection to the
+ * runtime
+ */
+ public AutoTier getAutoTier() {
+ return autoTier;
+ }
+
+ /**
+ * Sets the routing tier, meaningful only with model {@code auto} (Auto mode
+ * V2). Requires a runtime version that supports {@code capi.autoTier}.
+ *
+ * When omitted, the runtime chooses its default on create and preserves the
+ * persisted or current tier on resume. An explicit tier overrides the persisted
+ * tier on cold resume; the runtime rejects a conflicting tier when resuming a
+ * session already resident in memory.
+ *
+ * @param autoTier
+ * the routing tier, or {@code null} to omit it from the request
+ * @return this config for method chaining
+ */
+ public CapiSessionOptions setAutoTier(AutoTier autoTier) {
+ this.autoTier = autoTier;
+ return this;
+ }
+
/**
* Gets whether CAPI Responses API WebSocket transport is enabled.
*
diff --git a/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java b/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java
index 17e8f131f7..dccb4e9add 100644
--- a/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java
+++ b/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java
@@ -9,12 +9,16 @@
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import com.fasterxml.jackson.databind.JsonNode;
+import com.github.copilot.rpc.AutoTier;
import com.github.copilot.rpc.CapiSessionOptions;
import com.github.copilot.rpc.ResumeSessionConfig;
import com.github.copilot.rpc.SessionConfig;
@@ -29,6 +33,7 @@ void defaultsAreNull() {
var capi = new CapiSessionOptions();
assertNull(capi.getEnableWebSocketResponses());
+ assertNull(capi.getAutoTier());
}
@Test
@@ -37,6 +42,8 @@ void fluentSetterReturnsSameInstance() {
assertSame(capi, capi.setEnableWebSocketResponses(true));
assertEquals(Boolean.TRUE, capi.getEnableWebSocketResponses());
+ assertSame(capi, capi.setAutoTier(AutoTier.BALANCE));
+ assertEquals(AutoTier.BALANCE, capi.getAutoTier());
}
@Test
@@ -46,6 +53,7 @@ void serializesEnableWebSocketResponses() {
JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi);
assertTrue(json.get("enableWebSocketResponses").asBoolean());
+ assertTrue(json.path("autoTier").isMissingNode());
}
@Test
@@ -55,6 +63,44 @@ void omitsUnsetEnableWebSocketResponses() {
JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi);
assertTrue(json.path("enableWebSocketResponses").isMissingNode());
+ assertTrue(json.path("autoTier").isMissingNode());
+ assertEquals(0, json.size());
+ }
+
+ @ParameterizedTest
+ @CsvSource({"EFFICIENCY,efficiency", "BALANCE,balance", "INTELLIGENCE,intelligence"})
+ void autoTierCanonicalValuesRoundTripAndForward(AutoTier tier, String value) throws Exception {
+ var mapper = JsonRpcClient.getObjectMapper();
+ var capi = new CapiSessionOptions().setAutoTier(tier);
+ JsonNode json = mapper.valueToTree(capi);
+ assertEquals(value, json.get("autoTier").asText());
+ assertEquals(1, json.size());
+ assertEquals(tier, mapper.treeToValue(json, CapiSessionOptions.class).getAutoTier());
+
+ capi.setEnableWebSocketResponses(false);
+ var create = SessionRequestBuilder.buildCreateRequest(new SessionConfig().setModel("auto").setCapi(capi),
+ "session-1");
+ var resume = SessionRequestBuilder.buildResumeRequest("session-1", new ResumeSessionConfig().setCapi(capi));
+ for (Object request : new Object[]{create, resume}) {
+ JsonNode requestJson = mapper.valueToTree(request);
+ assertEquals(value, requestJson.get("capi").get("autoTier").asText());
+ assertFalse(requestJson.get("capi").get("enableWebSocketResponses").asBoolean());
+ assertEquals(2, requestJson.get("capi").size());
+ }
+ }
+
+ @Test
+ void autoTierRejectsNoncanonicalValues() {
+ for (String value : new String[]{"balanced", "Balance", "unknown"}) {
+ assertThrows(IllegalArgumentException.class, () -> AutoTier.fromValue(value));
+ }
+ assertNull(AutoTier.fromValue(null));
+ }
+
+ @Test
+ void clearingAutoTierOmitsIt() {
+ var capi = new CapiSessionOptions().setAutoTier(AutoTier.BALANCE).setAutoTier(null);
+ JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi);
assertEquals(0, json.size());
}
@@ -67,6 +113,7 @@ void createRequestIncludesCapiWhenSet() {
assertNotNull(request.getCapi());
assertTrue(json.get("capi").get("enableWebSocketResponses").asBoolean());
+ assertTrue(json.get("capi").path("autoTier").isMissingNode());
}
@Test
@@ -89,6 +136,7 @@ void resumeRequestIncludesCapiWhenSet() {
assertNotNull(request.getCapi());
assertTrue(json.get("capi").get("enableWebSocketResponses").asBoolean());
+ assertTrue(json.get("capi").path("autoTier").isMissingNode());
}
@Test
diff --git a/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java b/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java
new file mode 100644
index 0000000000..5213cdbb8d
--- /dev/null
+++ b/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java
@@ -0,0 +1,67 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import com.github.copilot.generated.AutoTier;
+import com.github.copilot.generated.SessionEvent;
+import com.github.copilot.generated.SessionResumeEvent;
+import com.github.copilot.generated.SessionStartEvent;
+
+/**
+ * Verifies auto routing preferences on generated session lifecycle events.
+ */
+class SessionAutoTierEventTest {
+
+ private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper();
+
+ @ParameterizedTest
+ @CsvSource({"session.start,EFFICIENCY,efficiency", "session.start,BALANCE,balance",
+ "session.start,INTELLIGENCE,intelligence", "session.resume,EFFICIENCY,efficiency",
+ "session.resume,BALANCE,balance", "session.resume,INTELLIGENCE,intelligence"})
+ void canonicalAutoTierRoundTrips(String type, AutoTier tier, String value) throws Exception {
+ String json = """
+ {"type":"%s","data":{"selectedModel":"auto","autoTier":"%s"}}
+ """.formatted(type, value);
+
+ var event = MAPPER.readValue(json, SessionEvent.class);
+ assertEquals(tier, autoTier(event, type));
+ String serialized = MAPPER.writeValueAsString(event);
+ assertEquals(value, MAPPER.readTree(serialized).path("data").path("autoTier").asText());
+ assertEquals(tier, autoTier(MAPPER.readValue(serialized, SessionEvent.class), type));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"session.start", "session.resume"})
+ void missingOrNullAutoTierRemainsOptional(String type) throws Exception {
+ for (String data : new String[]{"{}", "{\"autoTier\":null}"}) {
+ String json = """
+ {"type":"%s","data":%s}
+ """.formatted(type, data);
+
+ var event = MAPPER.readValue(json, SessionEvent.class);
+ assertNull(autoTier(event, type));
+ var serialized = MAPPER.readTree(MAPPER.writeValueAsString(event));
+ assertFalse(serialized.path("data").has("autoTier"));
+ }
+ }
+
+ private static AutoTier autoTier(SessionEvent event, String type) {
+ if ("session.start".equals(type)) {
+ return assertInstanceOf(SessionStartEvent.class, event).getData().autoTier();
+ }
+ return assertInstanceOf(SessionResumeEvent.class, event).getData().autoTier();
+ }
+}
diff --git a/nodejs/README.md b/nodejs/README.md
index 53e81c7aa4..0407c9dba4 100644
--- a/nodejs/README.md
+++ b/nodejs/README.md
@@ -131,6 +131,7 @@ Create a new conversation session.
- `sessionId?: string` - Custom session ID.
- `model?: string` - Model to use ("gpt-5", "claude-sonnet-4.5", etc.). **Required when using custom provider.**
+- `capi?: CapiSessionOptions` - Copilot API options. With `model: "auto"`, set `autoTier` to `"efficiency"`, `"balance"`, or `"intelligence"` to choose a routing preference. Requires a runtime with Auto tier support and V2 Auto routing. Omission preserves default behavior. See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for resume semantics.
- `reasoningEffort?: "low" | "medium" | "high" | "xhigh" | "max"` - Reasoning effort level for models that support it. Use `listModels()` to check which models support this option.
- `tools?: Tool[]` - Custom tools exposed to the CLI. Tools without `handler` are declaration-only and must be resolved via pending tool-call RPCs.
- `systemMessage?: SystemMessageConfig` - System message customization (see below)
diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts
index 8b9095e0ba..11e742bee7 100644
--- a/nodejs/src/index.ts
+++ b/nodejs/src/index.ts
@@ -120,6 +120,7 @@ export type {
ModelBilling,
ModelBillingTokenPrices,
ModelBillingTokenPricesLongContext,
+ AutoTier,
CapiSessionOptions,
ModelCapabilities,
ModelCapabilitiesOverride,
diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts
index e6f90490ed..f1b321b715 100644
--- a/nodejs/src/types.ts
+++ b/nodejs/src/types.ts
@@ -11,6 +11,7 @@ import type { Canvas } from "./canvas.js";
import type { SessionFsProvider } from "./sessionFsProvider.js";
import type { CopilotRequestHandler } from "./copilotRequestHandler.js";
import type {
+ AutoTier,
PermissionRequest as GeneratedPermissionRequest,
PermissionRequestedData as GeneratedPermissionRequestedData,
PermissionRequestedEvent as GeneratedPermissionRequestedEvent,
@@ -72,7 +73,7 @@ export type {
export type SessionEvent =
| Exclude
| PermissionRequestedEvent;
-export type { ReasoningSummary } from "./generated/session-events.js";
+export type { AutoTier, ReasoningSummary } from "./generated/session-events.js";
export type { SessionFsProvider } from "./sessionFsProvider.js";
export { createSessionFsAdapter } from "./sessionFsProvider.js";
export type { SessionFsFileInfo } from "./sessionFsProvider.js";
@@ -2129,6 +2130,17 @@ export interface FactoryMeta {
* provider-level choices are conceptually per-provider rather than global.
*/
export interface CapiSessionOptions {
+ /**
+ * Routing preference used when the session model is `auto`.
+ * Requires a runtime with Auto tier support and V2 Auto routing.
+ *
+ * When omitted on create, the runtime uses its default routing behavior.
+ * The runtime persists this preference across cold resume; an explicit tier
+ * on cold resume overrides the persisted value. For an already-resident
+ * session, omission preserves the current tier and a different tier is rejected.
+ */
+ autoTier?: AutoTier;
+
/**
* Whether to use the WebSocket transport for the CAPI Responses API.
*
diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts
index 5baff82cb4..da8a63b628 100644
--- a/nodejs/test/client.test.ts
+++ b/nodejs/test/client.test.ts
@@ -12,6 +12,7 @@ import {
createCanvas,
DisableBypassPermissionsModes,
RuntimeConnection,
+ type CapiSessionOptions,
type GitHubTelemetryNotification,
type ManagedSettings,
type ModelInfo,
@@ -1309,37 +1310,50 @@ describe("CopilotClient", () => {
expect(resumePayload.expAssignments).toBeUndefined();
});
- it("forwards capi options in session.create and session.resume", async () => {
- const client = new CopilotClient();
- await client.start();
- onTestFinished(() => stopClient(client));
+ it.each([
+ undefined,
+ {},
+ { enableWebSocketResponses: false },
+ { enableWebSocketResponses: true },
+ { autoTier: "efficiency" },
+ { autoTier: "balance" },
+ { autoTier: "intelligence" },
+ { autoTier: "balance", enableWebSocketResponses: false },
+ ] satisfies (CapiSessionOptions | undefined)[])(
+ "forwards capi options %j in session.create and session.resume",
+ async (capi) => {
+ const client = new CopilotClient();
+ await client.start();
+ onTestFinished(() => stopClient(client));
- const spy = vi
- .spyOn((client as any).connection!, "sendRequest")
- .mockImplementation(async (method: string, params: any) => {
- if (method === "session.create") return { sessionId: params.sessionId };
- if (method === "session.resume") return { sessionId: params.sessionId };
- throw new Error(`Unexpected method: ${method}`);
- });
+ const spy = vi
+ .spyOn((client as any).connection!, "sendRequest")
+ .mockImplementation(async (method: string, params: any) => {
+ if (method === "session.create") return { sessionId: params.sessionId };
+ if (method === "session.resume") return { sessionId: params.sessionId };
+ throw new Error(`Unexpected method: ${method}`);
+ });
- const session = await client.createSession({
- onPermissionRequest: approveAll,
- capi: { enableWebSocketResponses: false },
- });
- await client.resumeSession(session.sessionId, {
- onPermissionRequest: approveAll,
- capi: { enableWebSocketResponses: false },
- });
+ const session = await client.createSession({
+ onPermissionRequest: approveAll,
+ model: "auto",
+ capi,
+ });
+ await client.resumeSession(session.sessionId, {
+ onPermissionRequest: approveAll,
+ capi,
+ });
- const createPayload = spy.mock.calls.find(
- ([method]) => method === "session.create"
- )![1] as any;
- const resumePayload = spy.mock.calls.find(
- ([method]) => method === "session.resume"
- )![1] as any;
- expect(createPayload.capi).toEqual({ enableWebSocketResponses: false });
- expect(resumePayload.capi).toEqual({ enableWebSocketResponses: false });
- });
+ const createPayload = spy.mock.calls.find(
+ ([method]) => method === "session.create"
+ )![1] as any;
+ const resumePayload = spy.mock.calls.find(
+ ([method]) => method === "session.resume"
+ )![1] as any;
+ expect(JSON.parse(JSON.stringify(createPayload)).capi).toEqual(capi);
+ expect(JSON.parse(JSON.stringify(resumePayload)).capi).toEqual(capi);
+ }
+ );
it("forwards pluginDirectories and largeOutput in session.create and session.resume", async () => {
const client = new CopilotClient();
diff --git a/nodejs/test/session-event-types.test.ts b/nodejs/test/session-event-types.test.ts
index 5c41f2216a..93edebfc80 100644
--- a/nodejs/test/session-event-types.test.ts
+++ b/nodejs/test/session-event-types.test.ts
@@ -21,6 +21,8 @@ import type { FactoryAgentOptions as WireFactoryAgentOptions } from "../src/gene
import type {
// The aggregate union; must still resolve via the package root.
SessionEvent,
+ AutoTier,
+ CapiSessionOptions,
PermissionRequest,
PermissionRequestedData,
PermissionRequestedEvent,
@@ -128,6 +130,32 @@ type _PermissionRequestedEventStaysAlignedWithSessionEventUnion = _AssertEqual<
const _permissionRequestedEventAlignmentCheck: _PermissionRequestedEventStaysAlignedWithSessionEventUnion = true;
describe("Session event type exports (#1156)", () => {
+ it.each(["efficiency", "balance", "intelligence", undefined] satisfies (
+ | AutoTier
+ | undefined
+ )[])("exposes Auto tier %s on start and resume data", (autoTier) => {
+ const start: StartData = {
+ copilotVersion: "1.0.82-1",
+ producer: "copilot-agent",
+ sessionId: "session-1",
+ startTime: "2026-08-28T00:00:00Z",
+ version: 1,
+ autoTier,
+ };
+ const resume: ResumeData = {
+ eventCount: 1,
+ resumeTime: "2026-08-28T00:01:00Z",
+ autoTier,
+ };
+ const capi: CapiSessionOptions = { autoTier: start.autoTier };
+ expect(capi.autoTier).toBe(autoTier);
+ expect(resume.autoTier).toBe(autoTier);
+ if (autoTier === undefined) {
+ expect(JSON.parse(JSON.stringify(start))).not.toHaveProperty("autoTier");
+ expect(JSON.parse(JSON.stringify(resume))).not.toHaveProperty("autoTier");
+ }
+ });
+
it("exposes the headline ToolExecutionStartData type with a usable shape", () => {
// This is the specific type called out in issue #1156. The annotation
// is the compile-time API-surface check; these assertions only validate
diff --git a/python/README.md b/python/README.md
index c130f21c65..4e55a5f1f5 100644
--- a/python/README.md
+++ b/python/README.md
@@ -272,6 +272,7 @@ finally:
These are passed as keyword arguments to `create_session()`:
- `model` (str): Model to use ("gpt-5", "claude-sonnet-4.5", etc.). **Required when using custom provider.**
+- `capi` (CapiSessionOptions): Copilot API options. With `model="auto"`, set `auto_tier` to `"efficiency"`, `"balance"`, or `"intelligence"` to choose a routing preference. Requires a runtime with Auto tier support and V2 Auto routing. Omission preserves default behavior. See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for resume semantics.
- `reasoning_effort` (str): Reasoning effort level for models that support it ("low", "medium", "high", "xhigh", "max"). Use `list_models()` to check which models support this option.
- `session_id` (str): Custom session ID
- `tools` (list): Custom tools exposed to the CLI. Tools with `handler=None` are declaration-only and must be resolved via pending tool-call RPCs.
diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py
index 4b3b9901ed..5325b937c5 100644
--- a/python/copilot/__init__.py
+++ b/python/copilot/__init__.py
@@ -30,6 +30,7 @@
)
from .client import (
AskUserVariant,
+ AutoTier,
CapiSessionOptions,
ChildProcessRuntimeConnection,
CloudSessionOptions,
@@ -231,6 +232,7 @@
"AutoModeSwitchRequest",
"AutoModeSwitchResponse",
"AskUserVariant",
+ "AutoTier",
"BUILTIN_TOOLS_ISOLATED",
"CanvasAction",
"CanvasDeclaration",
diff --git a/python/copilot/client.py b/python/copilot/client.py
index 05412246de..2f36438ad2 100644
--- a/python/copilot/client.py
+++ b/python/copilot/client.py
@@ -262,9 +262,23 @@ def _exp_assignment_response_to_dict(
return wire
+AutoTier = Literal["efficiency", "balance", "intelligence"]
+"""Routing preference used when the session model is ``auto``."""
+
+
class CapiSessionOptions(TypedDict, total=False):
"""Provider-scoped Copilot API (CAPI) session options."""
+ auto_tier: AutoTier
+ """Routing preference used when the session model is ``auto``.
+
+ Requires a runtime with Auto tier support and V2 Auto routing. When omitted
+ on create, the runtime uses its default routing behavior. The runtime persists
+ this preference across cold resume; an explicit tier on cold resume overrides
+ the persisted value. For an already-resident session, omission preserves the
+ current tier and a different tier is rejected.
+ """
+
enable_web_socket_responses: bool
"""Whether to use WebSocket transport for the CAPI Responses API.
@@ -291,6 +305,8 @@ def _cloud_session_options_to_dict(options: CloudSessionOptions) -> dict[str, An
def _capi_session_options_to_wire(options: CapiSessionOptions) -> dict[str, Any]:
wire: dict[str, Any] = {}
+ if "auto_tier" in options:
+ wire["autoTier"] = options["auto_tier"]
if "enable_web_socket_responses" in options:
wire["enableWebSocketResponses"] = options["enable_web_socket_responses"]
return wire
@@ -2321,7 +2337,9 @@ async def create_session(
hooks: Lifecycle hooks for the session.
working_directory: Working directory for the session.
provider: Provider configuration for Azure or custom endpoints.
- capi: CAPI provider-scoped options. WebSocket transport is the
+ capi: CAPI provider-scoped options. Set ``auto_tier`` to ``efficiency``,
+ ``balance``, or ``intelligence`` to select an Auto routing preference
+ on a runtime with Auto tier support. WebSocket transport is the
default for the CAPI Responses API whenever the model advertises
the ``ws:/responses`` endpoint. Set
``enable_web_socket_responses=False`` to force the HTTP
@@ -3092,7 +3110,10 @@ async def resume_session(
hooks: Lifecycle hooks for the session.
working_directory: Working directory for the session.
provider: Provider configuration for Azure or custom endpoints.
- capi: CAPI provider-scoped options. WebSocket transport is the
+ capi: CAPI provider-scoped options. Omit ``auto_tier`` to preserve the
+ current or persisted Auto routing preference. An explicit tier
+ overrides it on cold resume, but cannot change it on an
+ already-resident session. WebSocket transport is the
default for the CAPI Responses API whenever the model advertises
the ``ws:/responses`` endpoint. Set
``enable_web_socket_responses=False`` to force the HTTP
diff --git a/python/test_client.py b/python/test_client.py
index bfcee83db2..d17e735ea9 100644
--- a/python/test_client.py
+++ b/python/test_client.py
@@ -1126,7 +1126,56 @@ async def mock_request(method, params, **kwargs):
await client.force_stop()
@pytest.mark.asyncio
- async def test_create_and_resume_session_forward_capi_options(self):
+ @pytest.mark.parametrize(
+ ("create_capi", "resume_capi", "expected_create", "expected_resume"),
+ [
+ (None, None, None, None),
+ ({}, {}, {}, {}),
+ (
+ {"enable_web_socket_responses": False},
+ {"enable_web_socket_responses": True},
+ {"enableWebSocketResponses": False},
+ {"enableWebSocketResponses": True},
+ ),
+ (
+ {"enable_web_socket_responses": True},
+ {"enable_web_socket_responses": False},
+ {"enableWebSocketResponses": True},
+ {"enableWebSocketResponses": False},
+ ),
+ (
+ {"auto_tier": "efficiency"},
+ {"auto_tier": "efficiency"},
+ {"autoTier": "efficiency"},
+ {"autoTier": "efficiency"},
+ ),
+ (
+ {"auto_tier": "balance"},
+ {"auto_tier": "balance"},
+ {"autoTier": "balance"},
+ {"autoTier": "balance"},
+ ),
+ (
+ {"auto_tier": "intelligence"},
+ {"auto_tier": "intelligence"},
+ {"autoTier": "intelligence"},
+ {"autoTier": "intelligence"},
+ ),
+ (
+ {"auto_tier": "balance", "enable_web_socket_responses": False},
+ {"auto_tier": "balance", "enable_web_socket_responses": True},
+ {"autoTier": "balance", "enableWebSocketResponses": False},
+ {"autoTier": "balance", "enableWebSocketResponses": True},
+ ),
+ ],
+ )
+ async def test_create_and_resume_session_forward_capi_options(
+ self,
+ create_capi: CapiSessionOptions | None,
+ resume_capi: CapiSessionOptions | None,
+ expected_create: dict[str, object] | None,
+ expected_resume: dict[str, object] | None,
+ ):
client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH))
await client.start()
try:
@@ -1143,11 +1192,9 @@ async def mock_request(method, params, **kwargs):
return {}
client._client.request = mock_request
- create_capi: CapiSessionOptions = {"enable_web_socket_responses": False}
- resume_capi: CapiSessionOptions = {"enable_web_socket_responses": True}
-
session = await client.create_session(
on_permission_request=PermissionHandler.approve_all,
+ model="auto",
capi=create_capi,
)
await client.resume_session(
@@ -1156,12 +1203,14 @@ async def mock_request(method, params, **kwargs):
capi=resume_capi,
)
- assert captured["session.create"]["capi"] == {
- "enableWebSocketResponses": False,
- }
- assert captured["session.resume"]["capi"] == {
- "enableWebSocketResponses": True,
- }
+ for method, expected in (
+ ("session.create", expected_create),
+ ("session.resume", expected_resume),
+ ):
+ if expected is None:
+ assert "capi" not in captured[method]
+ else:
+ assert captured[method]["capi"] == expected
finally:
await client.force_stop()
diff --git a/python/test_event_forward_compatibility.py b/python/test_event_forward_compatibility.py
index 2e8015a97d..00cad43217 100644
--- a/python/test_event_forward_compatibility.py
+++ b/python/test_event_forward_compatibility.py
@@ -14,6 +14,7 @@
from copilot.session_events import (
AttachmentGitHubReferenceType,
+ AutoTier,
Data,
ElicitationCompletedAction,
ElicitationRequestedMode,
@@ -24,6 +25,8 @@
PermissionRequestMemoryAction,
SessionEventType,
SessionManagedSettingsResolvedData,
+ SessionResumeData,
+ SessionStartData,
SessionTaskCompleteData,
UserMessageAgentMode,
session_event_from_dict,
@@ -34,6 +37,40 @@
class TestEventForwardCompatibility:
"""Test forward compatibility for unknown event types."""
+ @pytest.mark.parametrize("event_type", ["session.start", "session.resume"])
+ @pytest.mark.parametrize("tier", ["efficiency", "balance", "intelligence", None])
+ def test_auto_tier_lifecycle_events_round_trip(self, event_type, tier):
+ timestamp = "2026-08-28T00:00:00Z"
+ data = (
+ {
+ "copilotVersion": "1.0.82-1",
+ "producer": "copilot-agent",
+ "sessionId": str(uuid4()),
+ "startTime": timestamp,
+ "version": 1,
+ }
+ if event_type == "session.start"
+ else {"eventCount": 1, "resumeTime": timestamp}
+ )
+ if tier is not None:
+ data["autoTier"] = tier
+ event = session_event_from_dict(
+ {
+ "id": str(uuid4()),
+ "timestamp": timestamp,
+ "parentId": None,
+ "type": event_type,
+ "data": data,
+ }
+ )
+ assert isinstance(event.data, (SessionStartData, SessionResumeData))
+ assert event.data.auto_tier == (AutoTier(tier) if tier is not None else None)
+ serialized = session_event_to_dict(event)["data"]
+ if tier is None:
+ assert "autoTier" not in serialized
+ else:
+ assert serialized["autoTier"] == tier
+
def test_session_usage_info_is_recognized(self):
"""The session.usage_info event type should be in the enum."""
assert SessionEventType.SESSION_USAGE_INFO.value == "session.usage_info"
diff --git a/rust/README.md b/rust/README.md
index 6114105c25..c047d71564 100644
--- a/rust/README.md
+++ b/rust/README.md
@@ -307,6 +307,30 @@ provider errors, and invalid token responses reject that operation instead of
falling back to ambient authentication. Idle sessions refresh only before their
next credential-consuming operation; there is no background refresh timer.
+### Auto routing tiers
+
+Use `CapiSessionOptions::with_auto_tier` to select `AutoTier::Efficiency`,
+`AutoTier::Balance`, or `AutoTier::Intelligence`. This option is meaningful only
+with model `auto` (Auto mode V2).
+It requires a runtime version that supports `capi.autoTier`.
+
+```rust
+use github_copilot_sdk::{AutoTier, CapiSessionOptions, SessionConfig};
+
+let config = SessionConfig::default()
+ .with_model("auto")
+ .with_capi(CapiSessionOptions::new().with_auto_tier(AutoTier::Balance));
+```
+
+The same options work with `ResumeSessionConfig::with_capi` and can be combined
+with `with_enable_web_socket_responses(false)`. The SDK omits an unset tier:
+the runtime chooses its default on create and preserves the persisted/current
+tier on resume. An explicit tier overrides the persisted tier on cold resume;
+the runtime rejects a conflicting tier when the session is already resident
+in memory. The SDK does not choose a default or manage tier persistence.
+See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence)
+for the lifecycle rules.
+
### Session Hooks
Hooks intercept CLI behavior at lifecycle points — tool use, prompt submission, session start/end, and errors. Install a `SessionHooks` impl with [`SessionConfig::with_hooks`] — the SDK auto-enables `hooks` in `SessionConfig` when one is set.
diff --git a/rust/src/types.rs b/rust/src/types.rs
index 726bc48f4c..98706bd734 100644
--- a/rust/src/types.rs
+++ b/rust/src/types.rs
@@ -21,6 +21,8 @@ pub use crate::copilot_request_handler::{
CopilotWebSocketResponse, WebSocketTransform, forward_http,
};
use crate::generated::api_types::{CurrentToolMetadata, OpenCanvasInstance};
+/// Routing tier for the `auto` model with Auto mode V2.
+pub use crate::generated::session_events::AutoTier;
use crate::generated::session_events::ReasoningSummary;
/// Context window tier for models that support tiered context windows.
pub use crate::generated::session_events::{ContextTier, SessionLimitsConfig};
@@ -1417,6 +1419,16 @@ impl ProviderConfig {
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct CapiSessionOptions {
+ /// Routing tier, meaningful only with model `auto` (Auto mode V2).
+ /// Requires a runtime version that supports `capi.autoTier`.
+ ///
+ /// When omitted, the runtime chooses its default on create and preserves
+ /// the persisted or current tier on resume. An explicit tier overrides the
+ /// persisted tier on cold resume; the runtime rejects a conflicting tier
+ /// when resuming a session already resident in memory.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub auto_tier: Option,
+
/// Whether to use WebSocket transport for CAPI Responses API calls.
///
/// When `Some(false)`, the runtime uses HTTP Responses transport even if
@@ -1432,6 +1444,12 @@ impl CapiSessionOptions {
Self::default()
}
+ /// Set the routing tier for the `auto` model (Auto mode V2).
+ pub fn with_auto_tier(mut self, auto_tier: AutoTier) -> Self {
+ self.auto_tier = Some(auto_tier);
+ self
+ }
+
/// Set whether to use WebSocket transport for CAPI Responses API calls.
pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
self.enable_web_socket_responses = Some(enable);
@@ -6006,9 +6024,9 @@ mod tests {
use super::{
AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
- AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
- CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, ExpConfigEntry,
- ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType,
+ AttachmentSelectionRange, AutoTier, AzureProviderOptions, CapiSessionOptions,
+ ConnectionState, CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode,
+ ExpConfigEntry, ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType,
InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig,
MemoryConfiguration, NamedProviderConfig, PermissionResponseCapability, ProviderConfig,
ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent,
@@ -7254,6 +7272,56 @@ mod tests {
let unset = CapiSessionOptions::new();
let wire_unset = serde_json::to_value(&unset).unwrap();
assert!(wire_unset.get("enableWebSocketResponses").is_none());
+ assert!(wire_unset.get("autoTier").is_none());
+ assert_eq!(wire_unset, json!({}));
+ }
+
+ #[test]
+ fn capi_auto_tier_canonical_values_round_trip_and_forward() {
+ for (tier, value) in [
+ (AutoTier::Efficiency, "efficiency"),
+ (AutoTier::Balance, "balance"),
+ (AutoTier::Intelligence, "intelligence"),
+ ] {
+ let exported: crate::AutoTier = tier.clone();
+ let capi = CapiSessionOptions::new().with_auto_tier(exported);
+ assert_eq!(capi.auto_tier, Some(tier));
+ assert_eq!(
+ serde_json::to_value(&capi).unwrap(),
+ json!({"autoTier": value})
+ );
+ assert_eq!(
+ serde_json::from_value::(json!({"autoTier": value})).unwrap(),
+ capi
+ );
+
+ let capi = capi.with_enable_web_socket_responses(false);
+ let expected = json!({"autoTier": value, "enableWebSocketResponses": false});
+ let (create, _) = SessionConfig::default()
+ .with_model("auto")
+ .with_capi(capi.clone())
+ .into_wire(Some(SessionId::from("capi-create")))
+ .unwrap();
+ assert_eq!(serde_json::to_value(create).unwrap()["capi"], expected);
+
+ let (resume, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
+ .with_capi(capi)
+ .into_wire()
+ .unwrap();
+ assert_eq!(serde_json::to_value(resume).unwrap()["capi"], expected);
+ }
+ }
+
+ #[test]
+ fn capi_auto_tier_accepts_unknown_values_for_forward_compatibility() {
+ for value in ["balanced", "Balance", "unknown"] {
+ assert_eq!(
+ serde_json::from_value::(json!(value)).unwrap(),
+ AutoTier::Unknown
+ );
+ }
+ let capi: CapiSessionOptions = serde_json::from_value(json!({})).unwrap();
+ assert_eq!(capi.auto_tier, None);
}
#[test]
diff --git a/rust/tests/api_types_test.rs b/rust/tests/api_types_test.rs
index 9b86b1367a..8ed40e7c76 100644
--- a/rust/tests/api_types_test.rs
+++ b/rust/tests/api_types_test.rs
@@ -3,11 +3,53 @@
#![allow(clippy::unwrap_used)]
+use github_copilot_sdk::AutoTier;
use github_copilot_sdk::rpc::{
Extension, ExtensionList, ExtensionSource, ExtensionStatus, ExtensionsDisableRequest,
ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, TasksStartAgentRequest,
};
-use github_copilot_sdk::session_events::{PermissionRequest, PermissionRequestedData};
+use github_copilot_sdk::session_events::{
+ PermissionRequest, PermissionRequestedData, SessionEventData, TypedSessionEvent,
+};
+
+#[test]
+fn session_events_deserialize_auto_tier() {
+ for event_type in ["session.start", "session.resume"] {
+ for (tier, wire_tier) in [
+ (Some(AutoTier::Efficiency), Some("efficiency")),
+ (Some(AutoTier::Balance), Some("balance")),
+ (Some(AutoTier::Intelligence), Some("intelligence")),
+ (None, None),
+ ] {
+ let mut wire = serde_json::json!({
+ "id": "11111111-1111-1111-1111-111111111111",
+ "timestamp": "2026-08-28T00:00:00Z",
+ "parentId": null,
+ "type": event_type,
+ "data": {
+ "sessionId": "test-session", "version": 1,
+ "producer": "copilot", "copilotVersion": "1.0.82-1",
+ "startTime": "2026-08-28T00:00:00Z",
+ "resumeTime": "2026-08-28T00:00:00Z", "eventCount": 1
+ }
+ });
+ if let Some(wire_tier) = wire_tier {
+ wire["data"]["autoTier"] = serde_json::json!(wire_tier);
+ }
+ let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
+ let actual: Option = match event.payload {
+ SessionEventData::SessionStart(data) if event_type == "session.start" => {
+ data.auto_tier
+ }
+ SessionEventData::SessionResume(data) if event_type == "session.resume" => {
+ data.auto_tier
+ }
+ _ => panic!("expected {event_type}"),
+ };
+ assert_eq!(actual, tier);
+ }
+ }
+}
#[test]
fn extension_running_has_expected_status_and_source() {