@hifullmoon/aicommit 2.2.3 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.aicommit.config.example.json +31 -8
- package/CHANGELOG.md +25 -1
- package/README.md +41 -6
- package/README.zh-CN.md +41 -6
- package/bin/aicommit.js +6 -1
- package/docs/distribution.md +8 -1
- package/docs/large-change-implementation-plan.md +193 -0
- package/docs/privacy.md +6 -0
- package/docs/provider-compatibility.md +44 -16
- package/docs/troubleshooting.md +12 -0
- package/package.json +4 -2
- package/schemas/aicommit-output.schema.json +120 -18
- package/src/analysis-budget.js +76 -0
- package/src/api.js +6 -316
- package/src/change-analysis.js +558 -0
- package/src/cli.js +31 -5
- package/src/completion.js +3 -0
- package/src/config.js +16 -0
- package/src/doctor.js +2 -4
- package/src/git-spool.js +108 -0
- package/src/git.js +25 -4
- package/src/local-analysis.js +271 -0
- package/src/main.js +120 -27
- package/src/model-client.js +403 -0
- package/src/provider-response.js +148 -0
- package/src/providers.js +134 -206
- package/src/runtime.js +6 -0
- package/src/split.js +194 -43
- package/src/update.js +326 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { EventSourceParserStream } from 'eventsource-parser/stream';
|
|
2
|
+
import { normalizeUsage } from './providers.js';
|
|
3
|
+
import { ERROR_CATEGORIES, fail } from './errors.js';
|
|
4
|
+
|
|
5
|
+
function textValue(value, separator = '\n') {
|
|
6
|
+
if (typeof value === 'string') return value;
|
|
7
|
+
if (Array.isArray(value))
|
|
8
|
+
return value
|
|
9
|
+
.map((part) => textValue(part, separator))
|
|
10
|
+
.filter(Boolean)
|
|
11
|
+
.join(separator);
|
|
12
|
+
if (value && typeof value === 'object')
|
|
13
|
+
return textValue(value.text ?? value.summary ?? value.content, separator);
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Some compatible endpoints ignore stream=true; native Ollama uses complete
|
|
18
|
+
// JSON here as before. Convert that single response into one SDK-readable event.
|
|
19
|
+
// The event normalizer below uses the same field mappings for real SSE responses.
|
|
20
|
+
export function completionEvent(data) {
|
|
21
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
22
|
+
throw fail(
|
|
23
|
+
ERROR_CATEGORIES.RESPONSE_FORMAT,
|
|
24
|
+
'Provider returned an invalid response: expected a JSON object.',
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
if (data.error)
|
|
28
|
+
throw fail(
|
|
29
|
+
ERROR_CATEGORIES.PROVIDER,
|
|
30
|
+
`Provider request failed: ${textValue(data.error.message) || JSON.stringify(data.error)}`,
|
|
31
|
+
);
|
|
32
|
+
const choice = data.choices?.[0];
|
|
33
|
+
const message = choice?.message ?? data.message;
|
|
34
|
+
const usage = normalizeUsage(data.usage || data);
|
|
35
|
+
const finish = choice?.finish_reason ?? data.stop_reason ?? data.done_reason ?? 'stop';
|
|
36
|
+
return {
|
|
37
|
+
model: data.model,
|
|
38
|
+
choices: [
|
|
39
|
+
{
|
|
40
|
+
index: 0,
|
|
41
|
+
delta: {
|
|
42
|
+
content:
|
|
43
|
+
textValue(message?.content, '') ||
|
|
44
|
+
textValue(data.content, '') ||
|
|
45
|
+
textValue(data.response, ''),
|
|
46
|
+
reasoning_content: reasoningDelta(message) || textValue(data.thinking),
|
|
47
|
+
},
|
|
48
|
+
finish_reason: normalizeFinishReason(finish),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
...(usage
|
|
52
|
+
? {
|
|
53
|
+
usage: {
|
|
54
|
+
prompt_tokens: usage.inputTokens,
|
|
55
|
+
completion_tokens: usage.outputTokens,
|
|
56
|
+
total_tokens: usage.totalTokens,
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
: {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Keep provider stop aliases out of Pi's error branch so the business recovery
|
|
64
|
+
// path receives a truncated result. Unknown and safety-related reasons stay intact.
|
|
65
|
+
function normalizeFinishReason(reason) {
|
|
66
|
+
if (['max_tokens', 'max_output_tokens', 'token_limit'].includes(reason)) return 'length';
|
|
67
|
+
return reason === 'end_turn' ? 'stop' : reason;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function detailText(value) {
|
|
71
|
+
if (Array.isArray(value)) return value.map(detailText).filter(Boolean).join('\n');
|
|
72
|
+
if (value?.type === 'reasoning.encrypted') return '';
|
|
73
|
+
return textValue(value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function reasoningDelta(delta) {
|
|
77
|
+
if (!delta) return '';
|
|
78
|
+
const primary =
|
|
79
|
+
textValue(delta.reasoning_content) ||
|
|
80
|
+
textValue(delta.reasoning) ||
|
|
81
|
+
textValue(delta.reasoning_text) ||
|
|
82
|
+
textValue(delta.thinking);
|
|
83
|
+
const details = detailText(delta.reasoning_details);
|
|
84
|
+
// Providers sometimes expose the same delta in both representations. Only
|
|
85
|
+
// deduplicate within this event: repeated text in a later delta can be intentional.
|
|
86
|
+
if (!primary) return details;
|
|
87
|
+
if (!details || primary.startsWith(details)) return primary;
|
|
88
|
+
if (details.startsWith(primary)) return details;
|
|
89
|
+
return primary + details;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeChunk(value) {
|
|
93
|
+
if (!value || !Array.isArray(value.choices)) return value;
|
|
94
|
+
for (const choice of value.choices) {
|
|
95
|
+
if (!choice || typeof choice !== 'object') continue;
|
|
96
|
+
choice.finish_reason = normalizeFinishReason(choice.finish_reason);
|
|
97
|
+
const delta = choice.delta ?? choice.message;
|
|
98
|
+
if (!delta || typeof delta !== 'object') continue;
|
|
99
|
+
choice.delta = { ...delta, reasoning_content: reasoningDelta(delta) };
|
|
100
|
+
// Retain reasoning_details for Pi's replay metadata while also exposing all
|
|
101
|
+
// its textual segments as ordinary thinking deltas, including legacy shapes.
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// eventsource-parser handles UTF-8-decoded SSE framing; this boundary adjusts
|
|
107
|
+
// only vendor fields. Pi still owns model-result assembly and finish validation.
|
|
108
|
+
// Web Stream piping preserves backpressure, cancellation and body read errors.
|
|
109
|
+
export function normalizeEventStream(response) {
|
|
110
|
+
if (!response.body)
|
|
111
|
+
throw fail(ERROR_CATEGORIES.RESPONSE_FORMAT, 'Streaming response did not include a body.');
|
|
112
|
+
let done = false;
|
|
113
|
+
const encoder = new globalThis.TextEncoder();
|
|
114
|
+
const body = response.body
|
|
115
|
+
.pipeThrough(new globalThis.TextDecoderStream())
|
|
116
|
+
.pipeThrough(new EventSourceParserStream())
|
|
117
|
+
.pipeThrough(
|
|
118
|
+
new globalThis.TransformStream({
|
|
119
|
+
transform(event, controller) {
|
|
120
|
+
if (done) return;
|
|
121
|
+
let data = event.data.trim();
|
|
122
|
+
if (data === '[DONE]') {
|
|
123
|
+
done = true;
|
|
124
|
+
} else {
|
|
125
|
+
let value;
|
|
126
|
+
try {
|
|
127
|
+
value = JSON.parse(data);
|
|
128
|
+
} catch (cause) {
|
|
129
|
+
throw fail(
|
|
130
|
+
ERROR_CATEGORIES.RESPONSE_FORMAT,
|
|
131
|
+
'Provider returned invalid JSON in streaming response.',
|
|
132
|
+
{ cause },
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
data = JSON.stringify(normalizeChunk(value));
|
|
136
|
+
}
|
|
137
|
+
controller.enqueue(
|
|
138
|
+
encoder.encode(`${event.event ? `event: ${event.event}\n` : ''}data: ${data}\n\n`),
|
|
139
|
+
);
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
const headers = new globalThis.Headers(response.headers);
|
|
144
|
+
headers.delete('content-length');
|
|
145
|
+
headers.delete('content-encoding');
|
|
146
|
+
headers.set('content-type', 'text/event-stream');
|
|
147
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers });
|
|
148
|
+
}
|
package/src/providers.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { clampThinkingLevel, getSupportedThinkingLevels } from '@earendil-works/pi-ai';
|
|
2
|
+
import { OPENAI_MODELS } from '@earendil-works/pi-ai/providers/openai.models';
|
|
3
|
+
import { DEEPSEEK_MODELS } from '@earendil-works/pi-ai/providers/deepseek.models';
|
|
4
|
+
import { OPENROUTER_MODELS } from '@earendil-works/pi-ai/providers/openrouter.models';
|
|
1
5
|
export const PROVIDER_TYPES = Object.freeze([
|
|
2
6
|
'openai',
|
|
3
7
|
'openrouter',
|
|
@@ -48,78 +52,6 @@ export function detectProviderType(apiUrl, explicitType = '') {
|
|
|
48
52
|
return 'custom';
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
export function isOpenAIReasoningModel(modelId) {
|
|
52
|
-
const id = (modelId || '').split('/').pop();
|
|
53
|
-
return /^(?:o\d|gpt-5)/i.test(id);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function openAIReasoningEfforts(modelId) {
|
|
57
|
-
const id = (modelId || '').split('/').pop().toLowerCase();
|
|
58
|
-
if (/^gpt-5\.6(?:-|$)/.test(id)) return ['none', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
59
|
-
if (/^gpt-5\.(?:2|3|4|5)(?:-|$)/.test(id)) {
|
|
60
|
-
return ['none', 'low', 'medium', 'high', 'xhigh'];
|
|
61
|
-
}
|
|
62
|
-
if (/^gpt-5\.1(?:-|$)/.test(id)) return ['none', 'low', 'medium', 'high'];
|
|
63
|
-
if (/^gpt-5(?:-|$)/.test(id) || /^o\d(?:-|$)/.test(id)) return ['low', 'medium', 'high'];
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Setup uses this to avoid offering an effort that a recognized official
|
|
68
|
-
// OpenAI model will reject. Other adapters either accept the common effort
|
|
69
|
-
// vocabulary, normalize it, or are model-dependent, so they retain the full
|
|
70
|
-
// list and let the user/provider make the final choice.
|
|
71
|
-
export function reasoningEffortsForModel(providerType, modelId) {
|
|
72
|
-
if ((providerType || '').toLowerCase() === 'openai') {
|
|
73
|
-
const supported = openAIReasoningEfforts(modelId);
|
|
74
|
-
if (supported) return supported.filter((effort) => effort !== 'none');
|
|
75
|
-
}
|
|
76
|
-
return [...REASONING_EFFORTS];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function canDisableReasoningForModel(providerType, modelId) {
|
|
80
|
-
if ((providerType || '').toLowerCase() !== 'openai') return true;
|
|
81
|
-
const supported = openAIReasoningEfforts(modelId);
|
|
82
|
-
return !supported || supported.includes('none');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function openAIReasoningEffort(modelId, enabled, effort) {
|
|
86
|
-
const requested = enabled ? effort : 'none';
|
|
87
|
-
const supported = openAIReasoningEfforts(modelId);
|
|
88
|
-
if (!supported || supported.includes(requested)) return requested;
|
|
89
|
-
|
|
90
|
-
const action = enabled ? `reasoning effort "${requested}"` : 'disabling reasoning';
|
|
91
|
-
throw new Error(
|
|
92
|
-
`OpenAI model "${modelId}" does not support ${action}. ` +
|
|
93
|
-
`Supported reasoning efforts: ${supported.join(', ')}.`,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function mergeRequestExtras(payload, extras) {
|
|
98
|
-
if (!extras || typeof extras !== 'object' || Array.isArray(extras)) return;
|
|
99
|
-
const { model: _model, messages: _messages, ...safe } = extras;
|
|
100
|
-
Object.assign(payload, safe);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function reasoningText(value) {
|
|
104
|
-
if (typeof value === 'string') return value;
|
|
105
|
-
if (Array.isArray(value)) return value.map(reasoningText).filter(Boolean).join('\n');
|
|
106
|
-
if (value && typeof value === 'object') {
|
|
107
|
-
return reasoningText(value.text ?? value.summary ?? value.content);
|
|
108
|
-
}
|
|
109
|
-
return '';
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function messageText(value) {
|
|
113
|
-
if (typeof value === 'string') return value;
|
|
114
|
-
if (Array.isArray(value)) {
|
|
115
|
-
return value
|
|
116
|
-
.map((part) => part?.text ?? part?.content ?? '')
|
|
117
|
-
.filter(Boolean)
|
|
118
|
-
.join('');
|
|
119
|
-
}
|
|
120
|
-
return value?.text ?? value?.content ?? '';
|
|
121
|
-
}
|
|
122
|
-
|
|
123
55
|
function firstNumber(...values) {
|
|
124
56
|
return values.find((value) => typeof value === 'number' && Number.isFinite(value));
|
|
125
57
|
}
|
|
@@ -154,172 +86,168 @@ export function normalizeUsage(usage) {
|
|
|
154
86
|
return Object.keys(normalized).length ? normalized : null;
|
|
155
87
|
}
|
|
156
88
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const choice = data?.choices?.[0];
|
|
163
|
-
const message = choice?.message ?? data.message;
|
|
164
|
-
const content =
|
|
165
|
-
messageText(message?.content) ||
|
|
166
|
-
messageText(data?.content?.[0]?.text) ||
|
|
167
|
-
messageText(data.response);
|
|
168
|
-
const reasoning =
|
|
169
|
-
reasoningText(message?.reasoning_content) ||
|
|
170
|
-
reasoningText(message?.reasoning) ||
|
|
171
|
-
reasoningText(message?.reasoning_details) ||
|
|
172
|
-
reasoningText(message?.thinking) ||
|
|
173
|
-
reasoningText(data.thinking) ||
|
|
174
|
-
null;
|
|
175
|
-
const finishReason =
|
|
176
|
-
choice?.finish_reason ?? data.stop_reason ?? data.done_reason ?? (data.done ? 'stop' : null);
|
|
177
|
-
const usageSource =
|
|
178
|
-
data.usage ||
|
|
179
|
-
(data.prompt_eval_count !== undefined || data.eval_count !== undefined
|
|
180
|
-
? {
|
|
181
|
-
prompt_eval_count: data.prompt_eval_count,
|
|
182
|
-
eval_count: data.eval_count,
|
|
183
|
-
}
|
|
184
|
-
: null);
|
|
185
|
-
|
|
186
|
-
return {
|
|
187
|
-
provider,
|
|
188
|
-
model: data.model || null,
|
|
189
|
-
content,
|
|
190
|
-
reasoning,
|
|
191
|
-
usage: normalizeUsage(usageSource),
|
|
192
|
-
finishReason,
|
|
193
|
-
raw: data,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function applyReasoning(payload, provider, modelId, reasoning, nativeOllama) {
|
|
198
|
-
const mode = reasoning?.mode || 'auto';
|
|
199
|
-
if (mode === 'auto') return;
|
|
200
|
-
|
|
201
|
-
const enabled = mode === 'on';
|
|
202
|
-
const effort = reasoning?.effort || DEFAULT_REASONING_EFFORT;
|
|
203
|
-
|
|
89
|
+
// Read Pi's bundled catalog locally. Credentials and network model discovery remain
|
|
90
|
+
// outside this layer; a configured model ID need not be present in the catalog.
|
|
91
|
+
function catalogModel(provider, modelId) {
|
|
204
92
|
if (provider === 'openai') {
|
|
205
|
-
|
|
206
|
-
payload.reasoning_effort = openAIReasoningEffort(modelId, enabled, effort);
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (provider === 'deepseek') {
|
|
211
|
-
delete payload.enable_thinking;
|
|
212
|
-
payload.thinking = { type: enabled ? 'enabled' : 'disabled' };
|
|
213
|
-
if (enabled) {
|
|
214
|
-
payload.reasoning_effort = effort === 'low' || effort === 'max' ? effort : 'high';
|
|
215
|
-
delete payload.temperature;
|
|
216
|
-
} else {
|
|
217
|
-
delete payload.reasoning_effort;
|
|
218
|
-
}
|
|
219
|
-
return;
|
|
93
|
+
return OPENAI_MODELS[modelId] || OPENAI_MODELS[modelId.replace(/-codex$/, '')];
|
|
220
94
|
}
|
|
95
|
+
if (provider === 'deepseek') return DEEPSEEK_MODELS[modelId];
|
|
96
|
+
if (provider === 'openrouter') return OPENROUTER_MODELS[modelId];
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
221
99
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
100
|
+
export function isOpenAIReasoningModel(modelId = '') {
|
|
101
|
+
return catalogModel('openai', modelId)?.reasoning ?? /^(?:o\d|gpt-5)/i.test(modelId);
|
|
102
|
+
}
|
|
226
103
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
} else {
|
|
233
|
-
delete payload.enable_thinking;
|
|
234
|
-
payload.thinking = { type: 'disabled' };
|
|
235
|
-
}
|
|
236
|
-
return;
|
|
104
|
+
export function reasoningEffortsForModel(providerType, modelId) {
|
|
105
|
+
const known = catalogModel((providerType || '').toLowerCase(), modelId);
|
|
106
|
+
if (known?.reasoning) {
|
|
107
|
+
const supported = getSupportedThinkingLevels(known);
|
|
108
|
+
return REASONING_EFFORTS.filter((effort) => supported.includes(effort));
|
|
237
109
|
}
|
|
110
|
+
return [...REASONING_EFFORTS];
|
|
111
|
+
}
|
|
238
112
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
113
|
+
export function canDisableReasoningForModel(providerType, modelId) {
|
|
114
|
+
const known = catalogModel((providerType || '').toLowerCase(), modelId);
|
|
115
|
+
return !known || getSupportedThinkingLevels(known).includes('off');
|
|
116
|
+
}
|
|
243
117
|
|
|
244
|
-
|
|
245
|
-
if (
|
|
118
|
+
function mergeRequestExtras(payload, extras) {
|
|
119
|
+
if (!extras || typeof extras !== 'object' || Array.isArray(extras)) return;
|
|
120
|
+
const { model: _model, messages: _messages, stream: _stream, ...safe } = extras;
|
|
121
|
+
Object.assign(payload, safe);
|
|
246
122
|
}
|
|
247
123
|
|
|
248
|
-
function
|
|
249
|
-
if (reasoning?.mode
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
124
|
+
function resolveEffort(provider, model, reasoning) {
|
|
125
|
+
if ((reasoning?.mode || 'auto') === 'auto' || !model.reasoning) return undefined;
|
|
126
|
+
const requested = reasoning.mode === 'on' ? reasoning.effort || DEFAULT_REASONING_EFFORT : 'off';
|
|
127
|
+
const known = catalogModel(provider, model.id);
|
|
128
|
+
if (
|
|
129
|
+
known &&
|
|
130
|
+
['openai', 'openrouter'].includes(provider) &&
|
|
131
|
+
!getSupportedThinkingLevels(known).includes(requested)
|
|
132
|
+
) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`${provider === 'openai' ? 'OpenAI' : 'OpenRouter'} model "${model.id}" does not support ${requested === 'off' ? 'disabling reasoning' : `reasoning effort "${requested}"`}. ` +
|
|
135
|
+
`Supported reasoning efforts: ${getSupportedThinkingLevels(known).join(', ')}.`,
|
|
136
|
+
);
|
|
256
137
|
}
|
|
257
|
-
|
|
258
|
-
return
|
|
138
|
+
const level = known && provider === 'deepseek' ? clampThinkingLevel(known, requested) : requested;
|
|
139
|
+
return level === 'off' ? undefined : level;
|
|
259
140
|
}
|
|
260
141
|
|
|
142
|
+
// The application selects a Pi model and applies only configuration compatibility
|
|
143
|
+
// overrides. Pi owns message conversion, model effort mapping and wire protocols.
|
|
261
144
|
export function getProviderAdapter({ apiUrl, providerType = '', modelId = '' }) {
|
|
262
145
|
const provider = detectProviderType(apiUrl, providerType);
|
|
263
|
-
const url = endpoint(apiUrl);
|
|
264
146
|
const nativeOllama =
|
|
265
|
-
provider === 'ollama' && /\/api\/(?:chat|generate)\/?$/i.test(
|
|
147
|
+
provider === 'ollama' && /\/api\/(?:chat|generate)\/?$/i.test(endpoint(apiUrl)?.pathname || '');
|
|
148
|
+
const known = catalogModel(provider, modelId);
|
|
266
149
|
const openAIReasoning = provider === 'openai' && isOpenAIReasoningModel(modelId);
|
|
267
|
-
|
|
150
|
+
const tokenField = openAIReasoning ? 'max_completion_tokens' : 'max_tokens';
|
|
151
|
+
const model = {
|
|
152
|
+
...known,
|
|
153
|
+
id: modelId,
|
|
154
|
+
name: known?.name || modelId,
|
|
155
|
+
api: 'openai-completions',
|
|
156
|
+
provider,
|
|
157
|
+
// The transport pins the full configured URL, including nonstandard proxy paths.
|
|
158
|
+
baseUrl: endpoint(apiUrl)?.origin || '',
|
|
159
|
+
reasoning:
|
|
160
|
+
known?.reasoning ?? (openAIReasoning || ['deepseek', 'openrouter'].includes(provider)),
|
|
161
|
+
input: ['text'],
|
|
162
|
+
cost: known?.cost || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
163
|
+
contextWindow: known?.contextWindow || 128000,
|
|
164
|
+
maxTokens: known?.maxTokens || 16384,
|
|
165
|
+
compat: {
|
|
166
|
+
...(known?.api === 'openai-completions' ? known.compat : {}),
|
|
167
|
+
supportsStore: false,
|
|
168
|
+
supportsDeveloperRole: false,
|
|
169
|
+
supportsReasoningEffort: ['openai', 'deepseek', 'openrouter'].includes(provider),
|
|
170
|
+
supportsUsageInStreaming: provider === 'openai',
|
|
171
|
+
supportsFinishReason: true,
|
|
172
|
+
maxTokensField: tokenField,
|
|
173
|
+
thinkingFormat:
|
|
174
|
+
provider === 'deepseek' ? 'deepseek' : provider === 'openrouter' ? 'openrouter' : 'openai',
|
|
175
|
+
},
|
|
176
|
+
};
|
|
268
177
|
const capabilities = Object.freeze({
|
|
269
178
|
streaming: !nativeOllama,
|
|
270
179
|
reasoning:
|
|
271
180
|
provider === 'custom'
|
|
272
181
|
? 'configurable'
|
|
273
|
-
:
|
|
182
|
+
: ['openai', 'openrouter', 'deepseek'].includes(provider) && !model.reasoning
|
|
274
183
|
? 'model-dependent'
|
|
275
184
|
: 'native',
|
|
276
|
-
tokenBudget: nativeOllama
|
|
277
|
-
? 'options.num_predict'
|
|
278
|
-
: openAIReasoning
|
|
279
|
-
? 'max_completion_tokens'
|
|
280
|
-
: 'max_tokens',
|
|
185
|
+
tokenBudget: nativeOllama ? 'options.num_predict' : tokenField,
|
|
281
186
|
usage: true,
|
|
282
187
|
finishReason: true,
|
|
283
188
|
});
|
|
284
|
-
|
|
285
189
|
return Object.freeze({
|
|
286
190
|
id: provider,
|
|
191
|
+
model,
|
|
192
|
+
nativeOllama,
|
|
287
193
|
capabilities,
|
|
288
194
|
headers: provider === 'openrouter' ? { 'X-Title': 'aicommit' } : {},
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
payload
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
195
|
+
options({ temperature, maxTokens, extraBody, reasoning }) {
|
|
196
|
+
const mode = reasoning?.mode || 'auto';
|
|
197
|
+
const reasoningEffort = resolveEffort(provider, model, reasoning);
|
|
198
|
+
return {
|
|
199
|
+
maxTokens,
|
|
200
|
+
temperature:
|
|
201
|
+
openAIReasoning || (provider === 'deepseek' && mode === 'on') ? undefined : temperature,
|
|
202
|
+
reasoningEffort,
|
|
203
|
+
cacheRetention: 'none',
|
|
204
|
+
onPayload(payload) {
|
|
205
|
+
// Pi's absent effort means "off"; AICommit's auto means server defaults.
|
|
206
|
+
const reasoningKeys = ['thinking', 'reasoning', 'reasoning_effort'];
|
|
207
|
+
const mapped = Object.fromEntries(
|
|
208
|
+
reasoningKeys
|
|
209
|
+
.filter((key) => payload[key] !== undefined)
|
|
210
|
+
.map((key) => [key, payload[key]]),
|
|
211
|
+
);
|
|
212
|
+
if (mode === 'auto') for (const key of reasoningKeys) delete payload[key];
|
|
213
|
+
mergeRequestExtras(payload, extraBody);
|
|
214
|
+
if (mode !== 'auto') {
|
|
215
|
+
if (model.reasoning) {
|
|
216
|
+
for (const key of reasoningKeys) delete payload[key];
|
|
217
|
+
Object.assign(payload, mapped);
|
|
218
|
+
if (provider === 'openai' && mode === 'off')
|
|
219
|
+
payload.reasoning_effort = model.thinkingLevelMap?.off || 'none';
|
|
220
|
+
}
|
|
221
|
+
if (provider === 'deepseek') {
|
|
222
|
+
delete payload.enable_thinking;
|
|
223
|
+
if (mode === 'on') delete payload.temperature;
|
|
224
|
+
} else if (provider === 'minimax') {
|
|
225
|
+
payload.reasoning_split = true;
|
|
226
|
+
delete payload.enable_thinking;
|
|
227
|
+
if (mode === 'off') payload.thinking = { type: 'disabled' };
|
|
228
|
+
else delete payload.thinking;
|
|
229
|
+
} else if (nativeOllama) {
|
|
230
|
+
payload.think = mode === 'on';
|
|
231
|
+
} else if (provider === 'custom' || provider === 'ollama') {
|
|
232
|
+
mergeRequestExtras(
|
|
233
|
+
payload,
|
|
234
|
+
mode === 'on' ? reasoning?.enabledBody : reasoning?.disabledBody,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (provider === 'openai') {
|
|
239
|
+
payload.stream_options = { ...payload.stream_options, include_usage: true };
|
|
240
|
+
}
|
|
241
|
+
payload.stream = true;
|
|
242
|
+
},
|
|
243
|
+
};
|
|
320
244
|
},
|
|
321
245
|
reasoningForFollowUp(reasoning) {
|
|
322
|
-
|
|
246
|
+
if (reasoning?.mode !== 'on') return reasoning;
|
|
247
|
+
if (provider !== 'custom' && canDisableReasoningForModel(provider, modelId))
|
|
248
|
+
return { ...reasoning, mode: 'off' };
|
|
249
|
+
if (reasoning.disabledBody !== undefined) return { ...reasoning, mode: 'off' };
|
|
250
|
+
return reasoning;
|
|
323
251
|
},
|
|
324
252
|
});
|
|
325
253
|
}
|