Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
提交
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/features/session-persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ When resuming a session, you can optionally reconfigure many settings. This is u
| `availableTools` | Restrict which tools are available |
| `excludedTools` | Disable specific tools |
| `provider` | Re-provide BYOK credentials (required for BYOK sessions) |
| `capi.autoTier` | Override the persisted Auto routing preference on cold resume only |
| `reasoningEffort` | Adjust reasoning effort level |
| `streaming` | Enable/disable streaming responses |
| `workingDirectory` | Change the working directory |
Expand All @@ -253,6 +254,21 @@ When resuming a session, you can optionally reconfigure many settings. This is u
| `disabledSkills` | Skills to disable |
| `infiniteSessions` | Configure infinite session behavior |

### Auto tier persistence

With `model: "auto"`, the optional `capi.autoTier` setting selects an Auto routing preference: `efficiency`, `balance`, or `intelligence`. In Python, use `capi={"auto_tier": "balance"}`. This requires Copilot CLI `1.0.82-1` or later with V2 Auto routing; V1 Auto requests are unchanged.

The runtime persists the selected tier, so applications do not need to resend it on every resume:

* Omitting the tier when creating a session uses the runtime's default routing behavior.
* A cold resume restores the persisted tier. Supplying an explicit tier overrides the restored value for the new activation.
* When resuming a session already resident in the runtime, omitting the tier preserves the current selection, supplying the same tier is a no-op, and supplying a different tier is rejected.
* Older sessions without a persisted tier retain default routing behavior.

Tier selection is not a live model-switch operation. The SDK forwards the preference; the runtime owns persistence and validation.

The `session.start` and `session.resume` events expose the selected tier in their optional `data.autoTier` field (`data.auto_tier` in Python). When no tier is selected, the field is omitted.

### Example: changing model on resume

```typescript
Expand Down
5 changes: 5 additions & 0 deletions dotnet/src/Generated/Rpc.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 74 additions & 0 deletions dotnet/src/Generated/SessionEvents.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2398,6 +2398,18 @@ public sealed class CapiSessionOptions
/// </remarks>
[JsonPropertyName("enableWebSocketResponses")]
public bool? EnableWebSocketResponses { get; set; }

/// <summary>
/// Routing tier for model <c>auto</c> with V2 Auto.
/// </summary>
/// <remarks>
/// Requires a runtime that supports Auto tiers; it has no effect outside V2 Auto.
/// When omitted, 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.
/// </remarks>
[JsonPropertyName("autoTier")]
public AutoTier? AutoTier { get; set; }
}

/// <summary>
Expand Down
88 changes: 88 additions & 0 deletions dotnet/test/Unit/ClientSessionLifetimeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,94 @@ public async Task CreateSessionAsync_Omits_CustomAgent_ReasoningEffort_When_Unse
Assert.False(agent.TryGetProperty("reasoningEffort", out _));
}

public static TheoryData<AutoTier, string, bool?> CapiAutoTiers => new()
{
{ AutoTier.Efficiency, "efficiency", null },
{ AutoTier.Balance, "balance", null },
{ AutoTier.Intelligence, "intelligence", null },
{ AutoTier.Efficiency, "efficiency", false },
{ AutoTier.Balance, "balance", false },
{ AutoTier.Intelligence, "intelligence", false },
};

[Theory]
[MemberData(nameof(CapiAutoTiers))]
public async Task SessionRequests_Serialize_CapiAutoTier(AutoTier tier, string expectedTier, bool? enableWebSocketResponses)
{
await using var server = await FakeCopilotServer.StartAsync();
await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
var capi = new CapiSessionOptions { AutoTier = tier, EnableWebSocketResponses = enableWebSocketResponses };

await using var created = await client.CreateSessionAsync(new SessionConfig
{
Model = "auto",
Capi = capi,
OnPermissionRequest = PermissionHandler.ApproveAll
});
await using var resumed = await client.ResumeSessionAsync("resume-with-auto-tier", new ResumeSessionConfig
{
Model = "auto",
Capi = capi,
OnPermissionRequest = PermissionHandler.ApproveAll
});

foreach (var method in new[] { "session.create", "session.resume" })
{
var request = Assert.Single(server.Requests, request => request.Method == method);
var serializedCapi = request.Params.GetProperty("capi");
Assert.Equal(expectedTier, serializedCapi.GetProperty("autoTier").GetString());
if (enableWebSocketResponses.HasValue)
{
Assert.Equal(enableWebSocketResponses.Value, serializedCapi.GetProperty("enableWebSocketResponses").GetBoolean());
}
else
{
Assert.False(serializedCapi.TryGetProperty("enableWebSocketResponses", out _));
}
}
}

[Theory]
[InlineData(false, null)]
[InlineData(true, null)]
[InlineData(true, false)]
public async Task SessionRequests_Omit_CapiAutoTier_WhenUnset(bool includeCapi, bool? enableWebSocketResponses)
{
await using var server = await FakeCopilotServer.StartAsync();
await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
var capi = includeCapi ? new CapiSessionOptions { EnableWebSocketResponses = enableWebSocketResponses } : null;

await using var created = await client.CreateSessionAsync(new SessionConfig
{
Model = "auto",
Capi = capi,
OnPermissionRequest = PermissionHandler.ApproveAll
});
await using var resumed = await client.ResumeSessionAsync("resume-without-auto-tier", new ResumeSessionConfig
{
Capi = capi,
OnPermissionRequest = PermissionHandler.ApproveAll
});

foreach (var method in new[] { "session.create", "session.resume" })
{
var request = Assert.Single(server.Requests, request => request.Method == method);
Assert.Equal(includeCapi, request.Params.TryGetProperty("capi", out var serializedCapi));
if (includeCapi)
{
Assert.False(serializedCapi.TryGetProperty("autoTier", out _));
if (enableWebSocketResponses.HasValue)
{
Assert.Equal(enableWebSocketResponses.Value, serializedCapi.GetProperty("enableWebSocketResponses").GetBoolean());
}
else
{
Assert.Empty(serializedCapi.EnumerateObject());
}
}
}
}

[Fact]
public async Task SessionRequests_Serialize_AdditionalDirectories()
{
Expand Down
39 changes: 39 additions & 0 deletions dotnet/test/Unit/SessionEventSerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,45 @@ namespace GitHub.Copilot.Test.Unit;

public class SessionEventSerializationTests
{
public static TheoryData<AutoTier?, string?> AutoTiers => new()
{
{ AutoTier.Efficiency, "efficiency" },
{ AutoTier.Balance, "balance" },
{ AutoTier.Intelligence, "intelligence" },
{ null, null },
};

[Theory]
[MemberData(nameof(AutoTiers))]
public void SessionEvent_Deserializes_AutoTier(AutoTier? expectedTier, string? wireTier)
{
foreach (var eventType in new[] { "session.start", "session.resume" })
{
var autoTierProperty = wireTier is null ? "" : $""", "autoTier": "{wireTier}" """;
var json = $$"""
{
"id": "11111111-1111-1111-1111-111111111111",
"timestamp": "2026-08-28T00:00:00Z",
"parentId": null,
"type": "{{eventType}}",
"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
{{autoTierProperty}}
}
}
""";

var sessionEvent = SessionEvent.FromJson(json);
var actualTier = eventType == "session.start"
? Assert.IsType<SessionStartEvent>(sessionEvent).Data.AutoTier
: Assert.IsType<SessionResumeEvent>(sessionEvent).Data.AutoTier;
Assert.Equal(expectedTier, actualTier);
}
}

public static TheoryData<SessionEvent, string> JsonElementBackedEvents => new()
{
{
Expand Down
Loading
Loading