Drop sampling params unsupported by the model

The common-pattern enrichment and tracker-mapping agents run on
reasoning models such as gpt-5-nano, which reject an explicit
temperature and fail the whole request with a 400 ("Unsupported
value: 'temperature' does not support 0.1 with this model"). The
model registry already records this capability, but nothing
consulted it before dispatch, and dated provider snapshots like
gpt-5-nano-2025-08-07 did not resolve in the registry.

Resolve dated snapshots to their undated base model in registry
Lookup, and sanitize each chat completion request in the LLM
client by omitting the sampling knobs the target model does not
accept (temperature, top_p, frequency/presence penalties, stop).
Unknown models are left untouched, so models absent from the
registry keep their current behavior.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-05-29 17:02:29 +02:00
parent 55302d18f0
commit dbd868679d
4 changed files with 229 additions and 1 deletions

View File

@@ -47,6 +47,24 @@ func (m *mockProvider) ChatCompletionStream(_ context.Context, _ *llm.ChatComple
return m.streamResp, m.streamErr
}
// capturingProvider records the request it received so tests can assert
// on the parameters the client forwarded to the provider.
type capturingProvider struct {
lastReq *llm.ChatCompletionRequest
chatResp *llm.ChatCompletionResponse
streamResp llm.ChatCompletionStream
}
func (p *capturingProvider) ChatCompletion(_ context.Context, req *llm.ChatCompletionRequest) (*llm.ChatCompletionResponse, error) {
p.lastReq = req
return p.chatResp, nil
}
func (p *capturingProvider) ChatCompletionStream(_ context.Context, req *llm.ChatCompletionRequest) (llm.ChatCompletionStream, error) {
p.lastReq = req
return p.streamResp, nil
}
type mockStream struct {
events []llm.ChatCompletionStreamEvent
idx int
@@ -358,6 +376,105 @@ func TestChatCompletion(t *testing.T) {
})
}
// ---------------------------------------------------------------------------
// Client — request sanitization
// ---------------------------------------------------------------------------
func TestChatCompletionSanitizesUnsupportedParameters(t *testing.T) {
t.Parallel()
temp := 0.1
topP := 0.9
freq := 0.5
pres := 0.5
t.Run("drops temperature for reasoning model snapshot", func(t *testing.T) {
t.Parallel()
provider := &capturingProvider{
chatResp: &llm.ChatCompletionResponse{
Model: "gpt-5-nano",
FinishReason: llm.FinishReasonStop,
Message: llm.Message{
Role: llm.RoleAssistant,
Parts: []llm.Part{llm.TextPart{Text: "ok"}},
},
},
}
client, _ := newTestClient(provider)
_, err := client.ChatCompletion(context.Background(), &llm.ChatCompletionRequest{
Model: "gpt-5-nano-2025-08-07",
Messages: []llm.Message{{Role: llm.RoleUser, Parts: []llm.Part{llm.TextPart{Text: "Hi"}}}},
Temperature: &temp,
TopP: &topP,
FrequencyPenalty: &freq,
PresencePenalty: &pres,
})
require.NoError(t, err)
require.NotNil(t, provider.lastReq)
assert.Nil(t, provider.lastReq.Temperature)
assert.Nil(t, provider.lastReq.TopP)
assert.Nil(t, provider.lastReq.FrequencyPenalty)
assert.Nil(t, provider.lastReq.PresencePenalty)
})
t.Run("keeps temperature for chat model", func(t *testing.T) {
t.Parallel()
provider := &capturingProvider{
chatResp: &llm.ChatCompletionResponse{
Model: "gpt-4o",
FinishReason: llm.FinishReasonStop,
Message: llm.Message{
Role: llm.RoleAssistant,
Parts: []llm.Part{llm.TextPart{Text: "ok"}},
},
},
}
client, _ := newTestClient(provider)
_, err := client.ChatCompletion(context.Background(), &llm.ChatCompletionRequest{
Model: "gpt-4o",
Messages: []llm.Message{{Role: llm.RoleUser, Parts: []llm.Part{llm.TextPart{Text: "Hi"}}}},
Temperature: &temp,
})
require.NoError(t, err)
require.NotNil(t, provider.lastReq)
require.NotNil(t, provider.lastReq.Temperature)
assert.InEpsilon(t, 0.1, *provider.lastReq.Temperature, 1e-9)
})
t.Run("leaves unknown model untouched", func(t *testing.T) {
t.Parallel()
provider := &capturingProvider{
chatResp: &llm.ChatCompletionResponse{
Model: "mystery-model",
FinishReason: llm.FinishReasonStop,
Message: llm.Message{
Role: llm.RoleAssistant,
Parts: []llm.Part{llm.TextPart{Text: "ok"}},
},
},
}
client, _ := newTestClient(provider)
_, err := client.ChatCompletion(context.Background(), &llm.ChatCompletionRequest{
Model: "mystery-model",
Messages: []llm.Message{{Role: llm.RoleUser, Parts: []llm.Part{llm.TextPart{Text: "Hi"}}}},
Temperature: &temp,
})
require.NoError(t, err)
require.NotNil(t, provider.lastReq)
require.NotNil(t, provider.lastReq.Temperature)
assert.InEpsilon(t, 0.1, *provider.lastReq.Temperature, 1e-9)
})
}
// ---------------------------------------------------------------------------
// Client — ChatCompletionStream
// ---------------------------------------------------------------------------