Make OpenAI-family strict mode routable and stop redacting payee words
Strict structured-output mode rejects uniqueItems, so every request to a gpt-5.6-family zero-data-retention endpoint failed with HTTP 400 behind a generic error; duplicates were already rejected server-side, so the keyword leaves the wire schemas, pinned by a strict-keyword allowlist test built from the ledger that hit this. The bare-BIC redaction pattern deleted every 8- and 11-letter word — Openbank, BAUMARKT, RACETRACKER — blinding the model to the payee it was asked to classify and tripping the unsafe-merchant check on honest answers. BICs now die only labeled or attached to their IBAN, account labels join the redaction secrets, an identifier-shaped merchant name degrades to a merchant-less proposal instead of failing the row, and a provider error inside an HTTP 200 envelope is reported as such (numeric code only) instead of as envelope corruption.
This commit is contained in:
@@ -241,10 +241,13 @@ func (c *Client) Classify(ctx context.Context, facts domain.Facts, data domain.D
|
||||
}
|
||||
if answer.NewMerchant != nil {
|
||||
name := strings.Join(strings.Fields(*answer.NewMerchant), " ")
|
||||
// An identifier-shaped or oversized name is dropped, never stored, but
|
||||
// the row keeps its independently enum-validated category and tags: a
|
||||
// legitimate payee whose spelling trips the redactor (observed in the
|
||||
// field) must not lose its whole classification.
|
||||
if !utf8.ValidString(name) || utf8.RuneCountInString(name) > 100 || normalize(name) == "" || normalize(clean(name)) != normalize(name) {
|
||||
return fail("AI proposed an unsafe merchant name")
|
||||
}
|
||||
if existing := duplicateMerchant(name, data.Merchants); existing != nil {
|
||||
// no merchant
|
||||
} else if existing := duplicateMerchant(name, data.Merchants); existing != nil {
|
||||
e.MerchantID = existing.ID
|
||||
} else {
|
||||
aliases := []string{}
|
||||
@@ -352,7 +355,22 @@ func (c *Client) complete(ctx context.Context, gate *ratelimit.Controller, r com
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
if json.Unmarshal(raw, &envelope) != nil || (len(envelope.Error) > 0 && string(envelope.Error) != "null") || len(envelope.Choices) != 1 {
|
||||
if json.Unmarshal(raw, &envelope) != nil {
|
||||
return "", errors.New("invalid AI response envelope")
|
||||
}
|
||||
if len(envelope.Error) > 0 && string(envelope.Error) != "null" {
|
||||
// The provider reported a failure inside an HTTP 200 envelope. Only
|
||||
// its numeric code is safe to surface; the message may quote content.
|
||||
var detail struct {
|
||||
Code int `json:"code"`
|
||||
}
|
||||
_ = json.Unmarshal(envelope.Error, &detail)
|
||||
if detail.Code != 0 {
|
||||
return "", fmt.Errorf("AI provider reported an error (code %d)", detail.Code)
|
||||
}
|
||||
return "", errors.New("AI provider reported an error")
|
||||
}
|
||||
if len(envelope.Choices) != 1 {
|
||||
return "", errors.New("invalid AI response envelope")
|
||||
}
|
||||
choice := envelope.Choices[0]
|
||||
|
||||
Reference in New Issue
Block a user