feat: make openAICompatibleProvider work with Infomaniak out of the box (0.3.0)
- response_format is omitted by default (Infomaniak rejects the legacy
json_object → HTTP 422); opt in via the new `responseFormat` option
({type:'json_object'} or a json_schema object). BREAKING for endpoints
that relied on the forced json_object.
- parse LLM JSON leniently (tolerate markdown fences / surrounding prose)
- fold strict-coherence rules into DEFAULT_SYSTEM_PROMPT (exact
placeholder<->mapping-key identity, values are originals, strict format,
mask value not adjacent label) → reliable output across models
Verified live against Gemma 4 (google/gemma-4-31B-it, Infomaniak v2): all
demo phrases anonymize with clean round-trips, no custom provider needed.
36 tests passing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@ describe('openAICompatibleProvider', () => {
|
||||
expect(openAICompatibleProvider({ ...opts, apiKey: '' }).isConfigured()).toBe(false);
|
||||
});
|
||||
|
||||
it('anonymize() parses {texte_anonymise, mapping} and posts temperature 0 + json_object', async () => {
|
||||
it('anonymize() parses {texte_anonymise, mapping} and posts temperature 0, no response_format by default', async () => {
|
||||
const fetchMock = mockFetchJson({ texte_anonymise: '[EMAIL_1]', mapping: { '[EMAIL_1]': 'a@b.ch' } });
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
@@ -31,10 +31,31 @@ describe('openAICompatibleProvider', () => {
|
||||
expect(url).toBe('https://api.example.com/v1/chat/completions');
|
||||
const body = JSON.parse(init.body);
|
||||
expect(body.temperature).toBe(0);
|
||||
expect(body.response_format).toEqual({ type: 'json_object' });
|
||||
expect(body.response_format).toBeUndefined(); // omitted by default (Infomaniak rejects json_object)
|
||||
expect(init.headers.Authorization).toBe('Bearer k');
|
||||
});
|
||||
|
||||
it('sends response_format only when the option is provided', async () => {
|
||||
const fetchMock = mockFetchJson({ texte_anonymise: 'x', mapping: {} });
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
await openAICompatibleProvider({ ...opts, responseFormat: { type: 'json_object' } }).anonymize('x');
|
||||
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
|
||||
expect(body.response_format).toEqual({ type: 'json_object' });
|
||||
});
|
||||
|
||||
it('parses JSON wrapped in a markdown code fence (lenient parsing)', async () => {
|
||||
const fenced = '```json\n{"texte_anonymise":"[EMAIL_1]","mapping":{"[EMAIL_1]":"a@b.ch"}}\n```';
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ choices: [{ message: { content: fenced } }] }),
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
const r = await openAICompatibleProvider(opts).anonymize('a@b.ch');
|
||||
expect(r).toEqual({ anon: '[EMAIL_1]', mapping: { '[EMAIL_1]': 'a@b.ch' }, legend: {} });
|
||||
});
|
||||
|
||||
it('parses the model legende into result.legend, keeping only string values', async () => {
|
||||
const fetchMock = mockFetchJson({
|
||||
texte_anonymise: '[PER_1.NOM:M]',
|
||||
|
||||
Reference in New Issue
Block a user