fix(decision-ollama): native JSON-läge utan thinking #445

Merged
admin merged 1 commits from fix/ollama-native-json into main 2026-11-02 16:52:32 +00:00
2 changed files with 44 additions and 22 deletions
@@ -100,7 +100,7 @@ export class DecisionOllamaGatewayClient {
let response: Response;
try {
response = await this.fetchImpl(`${this.baseUrl}/v1/chat/completions`, {
response = await this.fetchImpl(`${this.baseUrl}/api/chat`, {
method: 'POST',
redirect: 'error',
signal: AbortSignal.timeout(this.options.timeoutMs ?? 120_000),
@@ -113,9 +113,20 @@ export class DecisionOllamaGatewayClient {
body: JSON.stringify({
model: this.options.model,
messages,
max_tokens: admission.maxOutputTokens,
temperature: 0,
stream: false,
// Structured extraction: JSON mode, no chain-of-thought output, and a
// hard output budget. A thinking model otherwise spends the whole
// budget on reasoning and returns model_incomplete.
format: 'json',
think: false,
options: {
temperature: 0,
num_predict: admission.maxOutputTokens,
num_ctx: Math.max(
4096,
Math.min(131072, admission.maxInputTokens + admission.maxOutputTokens + 1024),
),
},
}),
});
} catch {
@@ -131,13 +142,15 @@ export class DecisionOllamaGatewayClient {
}
const result = decoded as {
model?: unknown;
choices?: Array<{ message?: { content?: unknown }; finish_reason?: unknown }>;
usage?: { prompt_tokens?: unknown; completion_tokens?: unknown; total_tokens?: unknown };
message?: { content?: unknown };
done?: unknown;
done_reason?: unknown;
prompt_eval_count?: unknown;
eval_count?: unknown;
} | null;
const content = result?.choices?.[0]?.message?.content;
const promptTokens = result?.usage?.prompt_tokens;
const completionTokens = result?.usage?.completion_tokens;
const totalTokens = result?.usage?.total_tokens;
const content = result?.message?.content;
const promptTokens = result?.prompt_eval_count;
const completionTokens = result?.eval_count;
if (
!result ||
typeof content !== 'string' ||
@@ -145,16 +158,15 @@ export class DecisionOllamaGatewayClient {
!result.model.trim() ||
!Number.isSafeInteger(promptTokens) ||
!Number.isSafeInteger(completionTokens) ||
!Number.isSafeInteger(totalTokens) ||
(promptTokens as number) < 0 ||
(promptTokens as number) > admission.maxInputTokens ||
(completionTokens as number) > admission.maxOutputTokens ||
(promptTokens as number) + (completionTokens as number) !== totalTokens
(completionTokens as number) > admission.maxOutputTokens
) {
throw new Inf0OutcomeUnknownError();
}
const totalTokens = (promptTokens as number) + (completionTokens as number);
const finish = result.choices?.[0]?.finish_reason;
const finish = result.done_reason;
return {
status:
finish === 'stop' && (completionTokens as number) > 0 && content.trim()
@@ -55,14 +55,21 @@ describe('decision server Ollama gateway', () => {
expect(() => assertTrustedDecisionOllamaUrl('http://user:pass@100.96.133.104:11434')).toThrow();
});
it('maps an OpenAI-compatible Ollama response into the INF0 receipt', async () => {
const c = client(async () =>
Response.json({
it('maps a native Ollama chat response into the INF0 receipt', async () => {
const c = client(async (url, init) => {
expect(String(url)).toMatch(/\/api\/chat$/);
const body = JSON.parse(String(init?.body));
expect(body.format).toBe('json');
expect(body.think).toBe(false);
return Response.json({
model: 'hermes-qwen3.5:9b',
choices: [{ message: { content: '{"value":{}}' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 12, completion_tokens: 8, total_tokens: 20 },
}),
);
message: { role: 'assistant', content: '{"value":{}}' },
done: true,
done_reason: 'stop',
prompt_eval_count: 12,
eval_count: 8,
});
});
await expect(
c.complete({ context, admission, policy, reservation: undefined, inputTokens: 12, messages }),
).resolves.toMatchObject({
@@ -77,8 +84,11 @@ describe('decision server Ollama gateway', () => {
const c = client(async () =>
Response.json({
model: 'hermes-qwen3.5:9b',
choices: [{ message: { content: 'x' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 99 },
message: { role: 'assistant', content: 'x' },
done: true,
done_reason: 'stop',
prompt_eval_count: 1,
eval_count: 5000,
}),
);
await expect(