Skip to content

Commit c434f4d

Browse files
Add E2E tests for ProviderConfig model and token limit overrides
Adds two end-to-end tests per SDK (Node, Python, Go, .NET) that exercise the new ProviderConfig fields against the replaying CAPI proxy: - should_forward_provider_wire_model_and_max_output_tokens: verifies wireModel overrides the wire request model and maxOutputTokens is forwarded as max_tokens. - should_use_provider_model_id_as_wire_model: verifies modelId acts as the wire model when wireModel is unspecified and SessionConfig.Model is omitted. Also adds MaxTokens to the Go and .NET ChatCompletionRequest harness types so the assertion is observable, and ships two shared snapshot YAMLs under test/snapshots/session_config/. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 09c17f7 commit c434f4d

8 files changed

Lines changed: 249 additions & 4 deletions

File tree

dotnet/test/E2E/SessionConfigE2ETests.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,56 @@ public async Task Should_Forward_Custom_Provider_Headers_On_Resume()
177177
await session2.DisposeAsync();
178178
}
179179

180+
[Fact]
181+
public async Task Should_Forward_Provider_Wire_Model_And_Max_Output_Tokens()
182+
{
183+
var session = await CreateSessionAsync(new SessionConfig
184+
{
185+
Model = "claude-sonnet-4.5",
186+
Provider = new ProviderConfig
187+
{
188+
Type = "openai",
189+
BaseUrl = Ctx.ProxyUrl,
190+
ApiKey = "test-provider-key",
191+
WireModel = "test-wire-model",
192+
MaxOutputTokens = 1024,
193+
},
194+
});
195+
196+
await session.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" });
197+
198+
var exchange = Assert.Single(await Ctx.GetExchangesAsync());
199+
Assert.Equal("test-wire-model", exchange.Request.Model);
200+
Assert.Equal(1024, exchange.Request.MaxTokens);
201+
202+
await session.DisposeAsync();
203+
}
204+
205+
[Fact]
206+
public async Task Should_Use_Provider_Model_Id_As_Wire_Model()
207+
{
208+
// ProviderConfig.ModelId drives both the runtime resolved model AND the wire model
209+
// when WireModel is not specified. Here SessionConfig.Model is intentionally omitted
210+
// so that ModelId is the only model source.
211+
var session = await CreateSessionAsync(new SessionConfig
212+
{
213+
Provider = new ProviderConfig
214+
{
215+
Type = "openai",
216+
BaseUrl = Ctx.ProxyUrl,
217+
ApiKey = "test-provider-key",
218+
ModelId = "claude-sonnet-4.5",
219+
},
220+
});
221+
222+
await session.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" });
223+
224+
var exchange = Assert.Single(await Ctx.GetExchangesAsync());
225+
Assert.Equal("claude-sonnet-4.5", exchange.Request.Model);
226+
227+
await session.DisposeAsync();
228+
}
229+
180230
[Fact]
181231
public async Task Should_Use_WorkingDirectory_For_Tool_Execution()
182232
{

dotnet/test/Harness/CapiProxy.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,8 @@ public record ParsedHttpExchange(
206206
public record ChatCompletionRequest(
207207
string Model,
208208
List<ChatCompletionMessage> Messages,
209-
List<ChatCompletionTool>? Tools);
209+
List<ChatCompletionTool>? Tools,
210+
[property: JsonPropertyName("max_tokens")] int? MaxTokens = null);
210211

211212
public record ChatCompletionMessage(
212213
string Role,

go/internal/e2e/session_config_e2e_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,85 @@ func TestSessionConfigExtrasE2E(t *testing.T) {
323323
}
324324
})
325325

326+
t.Run("should forward provider wire model and max output tokens", func(t *testing.T) {
327+
ctx.ConfigureForTest(t)
328+
329+
maxOutputTokens := 1024
330+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
331+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
332+
Model: "claude-sonnet-4.5",
333+
Provider: &copilot.ProviderConfig{
334+
Type: "openai",
335+
BaseURL: ctx.ProxyURL,
336+
APIKey: "test-provider-key",
337+
WireModel: "test-wire-model",
338+
MaxOutputTokens: maxOutputTokens,
339+
},
340+
})
341+
if err != nil {
342+
t.Fatalf("CreateSession failed: %v", err)
343+
}
344+
345+
_, err = session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: "What is 1+1?"})
346+
if err != nil {
347+
t.Fatalf("SendAndWait failed: %v", err)
348+
}
349+
350+
exchanges, err := ctx.GetExchanges()
351+
if err != nil {
352+
t.Fatalf("GetExchanges failed: %v", err)
353+
}
354+
if len(exchanges) != 1 {
355+
t.Fatalf("Expected exactly 1 exchange, got %d", len(exchanges))
356+
}
357+
if exchanges[0].Request.Model != "test-wire-model" {
358+
t.Errorf("Expected request model to be 'test-wire-model', got %q", exchanges[0].Request.Model)
359+
}
360+
if exchanges[0].Request.MaxTokens == nil || *exchanges[0].Request.MaxTokens != 1024 {
361+
got := "nil"
362+
if exchanges[0].Request.MaxTokens != nil {
363+
got = fmt.Sprintf("%d", *exchanges[0].Request.MaxTokens)
364+
}
365+
t.Errorf("Expected request max_tokens to be 1024, got %s", got)
366+
}
367+
})
368+
369+
t.Run("should use provider model id as wire model", func(t *testing.T) {
370+
// ProviderConfig.ModelID drives both the runtime resolved model AND the wire
371+
// model when WireModel is not specified. SessionConfig.Model is intentionally
372+
// omitted so that ModelID is the only model source.
373+
ctx.ConfigureForTest(t)
374+
375+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
376+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
377+
Provider: &copilot.ProviderConfig{
378+
Type: "openai",
379+
BaseURL: ctx.ProxyURL,
380+
APIKey: "test-provider-key",
381+
ModelID: "claude-sonnet-4.5",
382+
},
383+
})
384+
if err != nil {
385+
t.Fatalf("CreateSession failed: %v", err)
386+
}
387+
388+
_, err = session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: "What is 1+1?"})
389+
if err != nil {
390+
t.Fatalf("SendAndWait failed: %v", err)
391+
}
392+
393+
exchanges, err := ctx.GetExchanges()
394+
if err != nil {
395+
t.Fatalf("GetExchanges failed: %v", err)
396+
}
397+
if len(exchanges) != 1 {
398+
t.Fatalf("Expected exactly 1 exchange, got %d", len(exchanges))
399+
}
400+
if exchanges[0].Request.Model != "claude-sonnet-4.5" {
401+
t.Errorf("Expected request model to be 'claude-sonnet-4.5', got %q", exchanges[0].Request.Model)
402+
}
403+
})
404+
326405
t.Run("should use workingDirectory for tool execution", func(t *testing.T) {
327406
ctx.ConfigureForTest(t)
328407

go/internal/e2e/testharness/proxy.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,10 @@ type ParsedHttpExchange struct {
167167

168168
// ChatCompletionRequest represents an OpenAI chat completion request.
169169
type ChatCompletionRequest struct {
170-
Model string `json:"model"`
171-
Messages []ChatCompletionMessage `json:"messages"`
172-
Tools []ChatCompletionTool `json:"tools,omitempty"`
170+
Model string `json:"model"`
171+
Messages []ChatCompletionMessage `json:"messages"`
172+
Tools []ChatCompletionTool `json:"tools,omitempty"`
173+
MaxTokens *int `json:"max_tokens,omitempty"`
173174
}
174175

175176
// ChatCompletionMessage represents a message in the chat completion request.

nodejs/test/e2e/session_config.e2e.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,52 @@ describe("Session Configuration", async () => {
325325
await session2.disconnect();
326326
});
327327

328+
it("should forward provider wire model and max output tokens", async () => {
329+
const session = await client.createSession({
330+
onPermissionRequest: approveAll,
331+
model: "claude-sonnet-4.5",
332+
provider: {
333+
type: "openai",
334+
baseUrl: openAiEndpoint.url,
335+
apiKey: "test-provider-key",
336+
wireModel: "test-wire-model",
337+
maxOutputTokens: 1024,
338+
},
339+
});
340+
341+
await session.sendAndWait({ prompt: "What is 1+1?" });
342+
343+
const exchanges = await openAiEndpoint.getExchanges();
344+
expect(exchanges.length).toBe(1);
345+
expect(exchanges[0].request.model).toBe("test-wire-model");
346+
expect((exchanges[0].request as { max_tokens?: number }).max_tokens).toBe(1024);
347+
348+
await session.disconnect();
349+
});
350+
351+
it("should use provider model id as wire model", async () => {
352+
// ProviderConfig.modelId drives both the runtime resolved model AND the wire
353+
// model when wireModel is not specified. SessionConfig.model is intentionally
354+
// omitted so that modelId is the only model source.
355+
const session = await client.createSession({
356+
onPermissionRequest: approveAll,
357+
provider: {
358+
type: "openai",
359+
baseUrl: openAiEndpoint.url,
360+
apiKey: "test-provider-key",
361+
modelId: "claude-sonnet-4.5",
362+
},
363+
});
364+
365+
await session.sendAndWait({ prompt: "What is 1+1?" });
366+
367+
const exchanges = await openAiEndpoint.getExchanges();
368+
expect(exchanges.length).toBe(1);
369+
expect(exchanges[0].request.model).toBe("claude-sonnet-4.5");
370+
371+
await session.disconnect();
372+
});
373+
328374
it("should apply workingDirectory on session resume", async () => {
329375
const subDir = join(workDir, "resume-subproject");
330376
await mkdir(subDir, { recursive: true });

python/e2e/test_session_config_e2e.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,53 @@ async def test_should_forward_custom_provider_headers_on_resume(self, ctx: E2ETe
236236
await session2.disconnect()
237237
await session1.disconnect()
238238

239+
async def test_should_forward_provider_wire_model_and_max_output_tokens( # noqa: E501
240+
self, ctx: E2ETestContext
241+
):
242+
session = await ctx.client.create_session(
243+
on_permission_request=PermissionHandler.approve_all,
244+
model="claude-sonnet-4.5",
245+
provider={
246+
"type": "openai",
247+
"base_url": ctx.proxy_url,
248+
"api_key": "test-provider-key",
249+
"wire_model": "test-wire-model",
250+
"max_output_tokens": 1024,
251+
},
252+
)
253+
254+
await session.send_and_wait("What is 1+1?")
255+
256+
exchanges = await ctx.get_exchanges()
257+
assert len(exchanges) == 1
258+
request = exchanges[0]["request"]
259+
assert request["model"] == "test-wire-model"
260+
assert request.get("max_tokens") == 1024
261+
262+
await session.disconnect()
263+
264+
async def test_should_use_provider_model_id_as_wire_model(self, ctx: E2ETestContext):
265+
# ProviderConfig.model_id drives both the runtime resolved model AND the wire
266+
# model when wire_model is not specified. SessionConfig.model is intentionally
267+
# omitted so that model_id is the only model source.
268+
session = await ctx.client.create_session(
269+
on_permission_request=PermissionHandler.approve_all,
270+
provider={
271+
"type": "openai",
272+
"base_url": ctx.proxy_url,
273+
"api_key": "test-provider-key",
274+
"model_id": "claude-sonnet-4.5",
275+
},
276+
)
277+
278+
await session.send_and_wait("What is 1+1?")
279+
280+
exchanges = await ctx.get_exchanges()
281+
assert len(exchanges) == 1
282+
assert exchanges[0]["request"]["model"] == "claude-sonnet-4.5"
283+
284+
await session.disconnect()
285+
239286
async def test_should_use_workingdirectory_for_tool_execution(self, ctx: E2ETestContext):
240287
sub_dir = os.path.join(ctx.work_dir, "subproject")
241288
os.makedirs(sub_dir, exist_ok=True)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
models:
2+
- claude-sonnet-4.5
3+
- test-wire-model
4+
conversations:
5+
- messages:
6+
- role: system
7+
content: ${system}
8+
- role: user
9+
content: What is 1+1?
10+
- role: assistant
11+
content: 1 + 1 = 2
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
models:
2+
- claude-sonnet-4.5
3+
conversations:
4+
- messages:
5+
- role: system
6+
content: ${system}
7+
- role: user
8+
content: What is 1+1?
9+
- role: assistant
10+
content: 1 + 1 = 2

0 commit comments

Comments
 (0)