omnikey-cli 1.2.0 → 1.3.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.
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+ /**
3
+ * Per-provider adapter tests for temperature handling.
4
+ *
5
+ * The three SDKs (`openai`, `@anthropic-ai/sdk`, `@google/genai`) are mocked
6
+ * at the module boundary using `vi.mock`. Mock spies are declared inside a
7
+ * `vi.hoisted()` block so they are available when `vi.mock` factories run
8
+ * (vi.mock is hoisted to the top of the file).
9
+ *
10
+ * These tests never contact any real API.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ const vitest_1 = require("vitest");
14
+ const mocks = vitest_1.vi.hoisted(() => ({
15
+ openaiCreate: vitest_1.vi.fn(),
16
+ anthropicCreate: vitest_1.vi.fn(),
17
+ anthropicStream: vitest_1.vi.fn(),
18
+ geminiGenerate: vitest_1.vi.fn(),
19
+ geminiGenerateStream: vitest_1.vi.fn(),
20
+ }));
21
+ vitest_1.vi.mock('openai', () => ({
22
+ default: class MockOpenAI {
23
+ constructor(_opts) {
24
+ this.chat = { completions: { create: mocks.openaiCreate } };
25
+ this.images = { generate: vitest_1.vi.fn() };
26
+ }
27
+ },
28
+ }));
29
+ vitest_1.vi.mock('@anthropic-ai/sdk', () => ({
30
+ default: class MockAnthropic {
31
+ constructor(_opts) {
32
+ this.messages = { create: mocks.anthropicCreate, stream: mocks.anthropicStream };
33
+ }
34
+ },
35
+ }));
36
+ vitest_1.vi.mock('@google/genai', () => ({
37
+ GoogleGenAI: class MockGoogleGenAI {
38
+ constructor(_opts) {
39
+ this.models = {
40
+ generateContent: mocks.geminiGenerate,
41
+ generateContentStream: mocks.geminiGenerateStream,
42
+ generateImages: vitest_1.vi.fn(),
43
+ };
44
+ }
45
+ },
46
+ // The adapter file imports these as types-only but they still need to resolve.
47
+ Content: class {
48
+ },
49
+ Tool: class {
50
+ },
51
+ }));
52
+ const ai_client_1 = require("../ai-client");
53
+ const messages = [{ role: 'user', content: 'hello' }];
54
+ function asAsyncIterable(chunks) {
55
+ return {
56
+ [Symbol.asyncIterator]: async function* () {
57
+ for (const c of chunks)
58
+ yield c;
59
+ },
60
+ };
61
+ }
62
+ (0, vitest_1.beforeEach)(() => {
63
+ mocks.openaiCreate.mockReset();
64
+ mocks.anthropicCreate.mockReset();
65
+ mocks.anthropicStream.mockReset();
66
+ mocks.geminiGenerate.mockReset();
67
+ mocks.geminiGenerateStream.mockReset();
68
+ });
69
+ // ---------------------------------------------------------------------------
70
+ // OpenAI
71
+ // ---------------------------------------------------------------------------
72
+ (0, vitest_1.describe)('OpenAIAdapter temperature handling', () => {
73
+ function mockCompleteResponse() {
74
+ mocks.openaiCreate.mockResolvedValueOnce({
75
+ choices: [{ message: { content: 'ok', tool_calls: undefined }, finish_reason: 'stop' }],
76
+ usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
77
+ });
78
+ }
79
+ function mockStreamResponse() {
80
+ mocks.openaiCreate.mockResolvedValueOnce(asAsyncIterable([
81
+ { choices: [{ delta: { content: 'ok' } }] },
82
+ {
83
+ choices: [{ delta: {} }],
84
+ usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
85
+ },
86
+ ]));
87
+ }
88
+ (0, vitest_1.it)('complete: passes temperature for gpt-4o-mini', async () => {
89
+ mockCompleteResponse();
90
+ const client = new ai_client_1.AIClient('openai', 'sk-test');
91
+ await client.complete('gpt-4o-mini', messages, { temperature: 0.42 });
92
+ const body = mocks.openaiCreate.mock.calls[0][0];
93
+ (0, vitest_1.expect)(body).toHaveProperty('temperature', 0.42);
94
+ });
95
+ (0, vitest_1.it)('complete: omits temperature for gpt-5.5 even if caller supplies one', async () => {
96
+ mockCompleteResponse();
97
+ const client = new ai_client_1.AIClient('openai', 'sk-test');
98
+ await client.complete('gpt-5.5', messages, { temperature: 0.42 });
99
+ const body = mocks.openaiCreate.mock.calls[0][0];
100
+ (0, vitest_1.expect)(body).not.toHaveProperty('temperature');
101
+ });
102
+ vitest_1.it.each(['gpt-5', 'gpt-5-mini', 'gpt-5.1', 'o1', 'o3-mini', 'o4-mini'])('complete: omits temperature for unsupported model %s', async (model) => {
103
+ mockCompleteResponse();
104
+ const client = new ai_client_1.AIClient('openai', 'sk-test');
105
+ await client.complete(model, messages, { temperature: 0.7 });
106
+ const body = mocks.openaiCreate.mock.calls[0][0];
107
+ (0, vitest_1.expect)(body).not.toHaveProperty('temperature');
108
+ });
109
+ (0, vitest_1.it)('streamComplete: passes temperature for gpt-4o-mini', async () => {
110
+ mockStreamResponse();
111
+ const client = new ai_client_1.AIClient('openai', 'sk-test');
112
+ await client.streamComplete('gpt-4o-mini', messages, { temperature: 0.31 }, () => { });
113
+ const body = mocks.openaiCreate.mock.calls[0][0];
114
+ (0, vitest_1.expect)(body).toHaveProperty('temperature', 0.31);
115
+ (0, vitest_1.expect)(body).toHaveProperty('stream', true);
116
+ });
117
+ (0, vitest_1.it)('streamComplete: omits temperature for gpt-5.5', async () => {
118
+ mockStreamResponse();
119
+ const client = new ai_client_1.AIClient('openai', 'sk-test');
120
+ await client.streamComplete('gpt-5.5', messages, { temperature: 0.31 }, () => { });
121
+ const body = mocks.openaiCreate.mock.calls[0][0];
122
+ (0, vitest_1.expect)(body).not.toHaveProperty('temperature');
123
+ (0, vitest_1.expect)(body).toHaveProperty('stream', true);
124
+ });
125
+ (0, vitest_1.it)('streamComplete: omits temperature even when caller passes empty options for gpt-5.5', async () => {
126
+ mockStreamResponse();
127
+ const client = new ai_client_1.AIClient('openai', 'sk-test');
128
+ await client.streamComplete('gpt-5.5', messages, {}, () => { });
129
+ const body = mocks.openaiCreate.mock.calls[0][0];
130
+ (0, vitest_1.expect)(body).not.toHaveProperty('temperature');
131
+ });
132
+ (0, vitest_1.it)('streamComplete: uses 0.3 default for supported model when caller omits temperature', async () => {
133
+ mockStreamResponse();
134
+ const client = new ai_client_1.AIClient('openai', 'sk-test');
135
+ await client.streamComplete('gpt-4o-mini', messages, {}, () => { });
136
+ const body = mocks.openaiCreate.mock.calls[0][0];
137
+ (0, vitest_1.expect)(body).toHaveProperty('temperature', 0.3);
138
+ });
139
+ });
140
+ // ---------------------------------------------------------------------------
141
+ // Anthropic
142
+ // ---------------------------------------------------------------------------
143
+ (0, vitest_1.describe)('AnthropicAdapter temperature handling', () => {
144
+ function mockCompleteResponse() {
145
+ mocks.anthropicCreate.mockResolvedValueOnce({
146
+ content: [{ type: 'text', text: 'ok' }],
147
+ stop_reason: 'end_turn',
148
+ usage: { input_tokens: 1, output_tokens: 1 },
149
+ });
150
+ }
151
+ function mockStreamResponse() {
152
+ const finalMessage = vitest_1.vi.fn().mockResolvedValue({
153
+ usage: { input_tokens: 1, output_tokens: 1 },
154
+ });
155
+ const stream = asAsyncIterable([
156
+ { type: 'content_block_delta', delta: { type: 'text_delta', text: 'ok' } },
157
+ ]);
158
+ stream.finalMessage = finalMessage;
159
+ mocks.anthropicStream.mockReturnValueOnce(stream);
160
+ }
161
+ (0, vitest_1.it)('complete: passes temperature for claude-sonnet-4-5', async () => {
162
+ mockCompleteResponse();
163
+ const client = new ai_client_1.AIClient('anthropic', 'sk-anthropic-test');
164
+ await client.complete('claude-sonnet-4-5', messages, { temperature: 0.42 });
165
+ const body = mocks.anthropicCreate.mock.calls[0][0];
166
+ (0, vitest_1.expect)(body).toHaveProperty('temperature', 0.42);
167
+ });
168
+ vitest_1.it.each([
169
+ 'claude-haiku-4-5-20251001',
170
+ 'claude-opus-4-5',
171
+ 'claude-opus-4-5-20251101',
172
+ 'claude-opus-4-6',
173
+ ])('complete: passes temperature for supported model %s', async (model) => {
174
+ mockCompleteResponse();
175
+ const client = new ai_client_1.AIClient('anthropic', 'sk-anthropic-test');
176
+ await client.complete(model, messages, { temperature: 0.5 });
177
+ const body = mocks.anthropicCreate.mock.calls[0][0];
178
+ (0, vitest_1.expect)(body).toHaveProperty('temperature', 0.5);
179
+ });
180
+ vitest_1.it.each(['claude-opus-4-7', 'claude-opus-4-7-20260101'])('complete: omits temperature for unsupported model %s', async (model) => {
181
+ mockCompleteResponse();
182
+ const client = new ai_client_1.AIClient('anthropic', 'sk-anthropic-test');
183
+ await client.complete(model, messages, { temperature: 0.5 });
184
+ const body = mocks.anthropicCreate.mock.calls[0][0];
185
+ (0, vitest_1.expect)(body).not.toHaveProperty('temperature');
186
+ });
187
+ (0, vitest_1.it)('streamComplete: passes temperature for claude-sonnet-4-5', async () => {
188
+ mockStreamResponse();
189
+ const client = new ai_client_1.AIClient('anthropic', 'sk-anthropic-test');
190
+ await client.streamComplete('claude-sonnet-4-5', messages, { temperature: 0.6 }, () => { });
191
+ const body = mocks.anthropicStream.mock.calls[0][0];
192
+ (0, vitest_1.expect)(body).toHaveProperty('temperature', 0.6);
193
+ });
194
+ (0, vitest_1.it)('streamComplete: omits temperature for claude-opus-4-7', async () => {
195
+ mockStreamResponse();
196
+ const client = new ai_client_1.AIClient('anthropic', 'sk-anthropic-test');
197
+ await client.streamComplete('claude-opus-4-7', messages, { temperature: 0.6 }, () => { });
198
+ const body = mocks.anthropicStream.mock.calls[0][0];
199
+ (0, vitest_1.expect)(body).not.toHaveProperty('temperature');
200
+ });
201
+ });
202
+ // ---------------------------------------------------------------------------
203
+ // Gemini
204
+ // ---------------------------------------------------------------------------
205
+ (0, vitest_1.describe)('GeminiAdapter temperature handling', () => {
206
+ function mockCompleteResponse() {
207
+ mocks.geminiGenerate.mockResolvedValueOnce({
208
+ candidates: [
209
+ {
210
+ content: { parts: [{ text: 'ok' }] },
211
+ finishReason: 'STOP',
212
+ },
213
+ ],
214
+ usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 1, totalTokenCount: 2 },
215
+ });
216
+ }
217
+ function mockStreamResponse() {
218
+ mocks.geminiGenerateStream.mockResolvedValueOnce(asAsyncIterable([
219
+ { text: 'ok' },
220
+ {
221
+ text: '',
222
+ usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 1, totalTokenCount: 2 },
223
+ },
224
+ ]));
225
+ }
226
+ vitest_1.it.each(['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-3-pro'])('complete: passes temperature for %s (all Gemini models accept it)', async (model) => {
227
+ mockCompleteResponse();
228
+ const client = new ai_client_1.AIClient('gemini', 'gemini-test-key');
229
+ await client.complete(model, messages, { temperature: 0.42 });
230
+ const body = mocks.geminiGenerate.mock.calls[0][0];
231
+ (0, vitest_1.expect)(body.config).toHaveProperty('temperature', 0.42);
232
+ });
233
+ (0, vitest_1.it)('streamComplete: passes temperature for gemini-2.5-pro', async () => {
234
+ mockStreamResponse();
235
+ const client = new ai_client_1.AIClient('gemini', 'gemini-test-key');
236
+ await client.streamComplete('gemini-2.5-pro', messages, { temperature: 0.31 }, () => { });
237
+ const body = mocks.geminiGenerateStream.mock.calls[0][0];
238
+ (0, vitest_1.expect)(body.config).toHaveProperty('temperature', 0.31);
239
+ });
240
+ (0, vitest_1.it)('streamComplete: applies default 0.3 when caller omits temperature', async () => {
241
+ mockStreamResponse();
242
+ const client = new ai_client_1.AIClient('gemini', 'gemini-test-key');
243
+ await client.streamComplete('gemini-2.5-pro', messages, {}, () => { });
244
+ const body = mocks.geminiGenerateStream.mock.calls[0][0];
245
+ (0, vitest_1.expect)(body.config).toHaveProperty('temperature', 0.3);
246
+ });
247
+ });
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const ai_client_1 = require("../ai-client");
5
+ (0, vitest_1.describe)('modelSupportsTemperature', () => {
6
+ (0, vitest_1.describe)('OpenAI', () => {
7
+ vitest_1.it.each([
8
+ ['gpt-4o-mini', true],
9
+ ['gpt-4o', true],
10
+ ['gpt-4-turbo', true],
11
+ ['gpt-3.5-turbo', true],
12
+ ])('allows temperature for %s', (model, expected) => {
13
+ (0, vitest_1.expect)((0, ai_client_1.modelSupportsTemperature)(model)).toBe(expected);
14
+ });
15
+ vitest_1.it.each([
16
+ ['gpt-5', false],
17
+ ['gpt-5-mini', false],
18
+ ['gpt-5.1', false],
19
+ ['gpt-5.5', false],
20
+ ['GPT-5.5', false], // case-insensitive
21
+ ])('rejects temperature for GPT-5 family member %s', (model, expected) => {
22
+ (0, vitest_1.expect)((0, ai_client_1.modelSupportsTemperature)(model)).toBe(expected);
23
+ });
24
+ vitest_1.it.each([
25
+ ['o1', false],
26
+ ['o1-preview', false],
27
+ ['o3', false],
28
+ ['o3-mini', false],
29
+ ['o4-mini', false],
30
+ ])('rejects temperature for reasoning model %s', (model, expected) => {
31
+ (0, vitest_1.expect)((0, ai_client_1.modelSupportsTemperature)(model)).toBe(expected);
32
+ });
33
+ });
34
+ (0, vitest_1.describe)('Gemini', () => {
35
+ vitest_1.it.each([
36
+ ['gemini-2.5-flash', true],
37
+ ['gemini-2.5-pro', true],
38
+ ['gemini-3-pro', true],
39
+ ['gemini-3.5-flash', true],
40
+ ])('allows temperature for %s', (model, expected) => {
41
+ (0, vitest_1.expect)((0, ai_client_1.modelSupportsTemperature)(model)).toBe(expected);
42
+ });
43
+ });
44
+ (0, vitest_1.describe)('Anthropic', () => {
45
+ vitest_1.it.each([
46
+ ['claude-haiku-4-5', true],
47
+ ['claude-haiku-4-5-20251001', true],
48
+ ['claude-sonnet-4-5', true],
49
+ ['claude-sonnet-4-5-20250929', true],
50
+ ['claude-opus-4-5', true],
51
+ ['claude-opus-4-5-20251101', true],
52
+ ['claude-opus-4-6', true],
53
+ ])('allows temperature for %s', (model, expected) => {
54
+ (0, vitest_1.expect)((0, ai_client_1.modelSupportsTemperature)(model)).toBe(expected);
55
+ });
56
+ vitest_1.it.each([
57
+ ['claude-opus-4-7', false],
58
+ ['claude-opus-4-7-20260101', false],
59
+ ['CLAUDE-OPUS-4-7', false], // case-insensitive
60
+ ])('rejects temperature for opus-4-7 variant %s', (model, expected) => {
61
+ (0, vitest_1.expect)((0, ai_client_1.modelSupportsTemperature)(model)).toBe(expected);
62
+ });
63
+ });
64
+ });
65
+ (0, vitest_1.describe)('getDefaultModel', () => {
66
+ (0, vitest_1.it)('returns the configured fast and smart tiers for each provider', () => {
67
+ // Don't pin exact model strings — they will be upgraded over time. Just
68
+ // assert that each provider returns a non-empty string for both tiers
69
+ // and that fast/smart differ (smart is meant to be a bigger model).
70
+ for (const provider of ['openai', 'gemini', 'anthropic']) {
71
+ const fast = (0, ai_client_1.getDefaultModel)(provider, 'fast');
72
+ const smart = (0, ai_client_1.getDefaultModel)(provider, 'smart');
73
+ (0, vitest_1.expect)(fast).toBeTruthy();
74
+ (0, vitest_1.expect)(smart).toBeTruthy();
75
+ (0, vitest_1.expect)(fast).not.toEqual(smart);
76
+ }
77
+ });
78
+ (0, vitest_1.it)('returns smart-tier models that are correctly classified by modelSupportsTemperature', () => {
79
+ // Regression guard: whenever a smart model is upgraded, the helper must
80
+ // continue to return the correct policy for it. This test is the single
81
+ // place that ties the two together so an accidental mismatch breaks the
82
+ // suite immediately.
83
+ const expectations = {
84
+ // OpenAI smart tier is in the GPT-5 family → no temperature.
85
+ openai: false,
86
+ // Gemini smart tier accepts temperature.
87
+ gemini: true,
88
+ // Anthropic smart tier is claude-opus-4-7 → no temperature.
89
+ anthropic: false,
90
+ };
91
+ for (const provider of Object.keys(expectations)) {
92
+ const expected = expectations[provider];
93
+ if (expected === null)
94
+ continue;
95
+ const smartModel = (0, ai_client_1.getDefaultModel)(provider, 'smart');
96
+ (0, vitest_1.expect)((0, ai_client_1.modelSupportsTemperature)(smartModel), `${provider} smart model "${smartModel}" should report temperature-support=${expected}`).toBe(expected);
97
+ }
98
+ });
99
+ });
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ /**
3
+ * Tests for the temperature-handling change in `runEnhancementModel`.
4
+ *
5
+ * - 'enhance' → { temperature: 0.3 }
6
+ * - 'grammar' → { temperature: 0.3 }
7
+ * - 'task' → {} (no temperature; smart-tier model decides for itself)
8
+ *
9
+ * Mocks `./ai-client` and `./models/subscriptionTaskTemplate` so the test
10
+ * stays a pure unit test and never touches the database or any SDK.
11
+ */
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ const vitest_1 = require("vitest");
17
+ const winston_1 = __importDefault(require("winston"));
18
+ const mocks = vitest_1.vi.hoisted(() => ({
19
+ streamComplete: vitest_1.vi.fn(),
20
+ getDefaultModel: vitest_1.vi.fn(),
21
+ findOne: vitest_1.vi.fn(),
22
+ }));
23
+ vitest_1.vi.mock('../ai-client', () => ({
24
+ aiClient: { streamComplete: mocks.streamComplete },
25
+ getDefaultModel: mocks.getDefaultModel,
26
+ }));
27
+ vitest_1.vi.mock('../models/subscriptionTaskTemplate', () => ({
28
+ SubscriptionTaskTemplate: { findOne: mocks.findOne },
29
+ }));
30
+ const featureRoutes_1 = require("../featureRoutes");
31
+ function makeLogger() {
32
+ return winston_1.default.createLogger({
33
+ silent: true,
34
+ transports: [new winston_1.default.transports.Console({ silent: true })],
35
+ });
36
+ }
37
+ const fakeSubscription = { id: 'sub_test' };
38
+ (0, vitest_1.beforeEach)(() => {
39
+ mocks.streamComplete.mockReset();
40
+ mocks.streamComplete.mockResolvedValue({ usage: undefined, model: 'mock-model' });
41
+ mocks.getDefaultModel.mockReset();
42
+ mocks.getDefaultModel.mockImplementation((_provider, tier) => tier === 'smart' ? 'smart-model-mock' : 'fast-model-mock');
43
+ mocks.findOne.mockReset();
44
+ // Default task template — plain text passes through `decompressString`
45
+ // so `getPromptForCommand('task', ...)` returns a non-empty prompt and the
46
+ // streamComplete path is reached.
47
+ mocks.findOne.mockResolvedValue({ instructions: 'You are a helpful task assistant.' });
48
+ });
49
+ (0, vitest_1.describe)('runEnhancementModel — temperature per command', () => {
50
+ (0, vitest_1.it)("passes temperature: 0.3 for cmd='enhance'", async () => {
51
+ const result = await (0, featureRoutes_1.runEnhancementModel)(makeLogger(), 'hello world', 'enhance', fakeSubscription);
52
+ (0, vitest_1.expect)(result).not.toBeNull();
53
+ (0, vitest_1.expect)(mocks.streamComplete).toHaveBeenCalledTimes(1);
54
+ const [, , options] = mocks.streamComplete.mock.calls[0];
55
+ (0, vitest_1.expect)(options).toEqual({ temperature: 0.3 });
56
+ });
57
+ (0, vitest_1.it)("passes temperature: 0.3 for cmd='grammar'", async () => {
58
+ const result = await (0, featureRoutes_1.runEnhancementModel)(makeLogger(), 'helo wrld', 'grammar', fakeSubscription);
59
+ (0, vitest_1.expect)(result).not.toBeNull();
60
+ (0, vitest_1.expect)(mocks.streamComplete).toHaveBeenCalledTimes(1);
61
+ const [, , options] = mocks.streamComplete.mock.calls[0];
62
+ (0, vitest_1.expect)(options).toEqual({ temperature: 0.3 });
63
+ });
64
+ (0, vitest_1.it)("omits temperature for cmd='task' (custom-task)", async () => {
65
+ const result = await (0, featureRoutes_1.runEnhancementModel)(makeLogger(), 'do the thing', 'task', fakeSubscription);
66
+ (0, vitest_1.expect)(result).not.toBeNull();
67
+ (0, vitest_1.expect)(mocks.streamComplete).toHaveBeenCalledTimes(1);
68
+ const [, , options] = mocks.streamComplete.mock.calls[0];
69
+ (0, vitest_1.expect)(options).toEqual({});
70
+ (0, vitest_1.expect)(options).not.toHaveProperty('temperature');
71
+ });
72
+ (0, vitest_1.it)("selects the smart-tier model for cmd='task' and fast-tier for enhance/grammar", async () => {
73
+ await (0, featureRoutes_1.runEnhancementModel)(makeLogger(), 'a', 'task', fakeSubscription);
74
+ await (0, featureRoutes_1.runEnhancementModel)(makeLogger(), 'b', 'enhance', fakeSubscription);
75
+ await (0, featureRoutes_1.runEnhancementModel)(makeLogger(), 'c', 'grammar', fakeSubscription);
76
+ const modelsCalled = mocks.streamComplete.mock.calls.map(([model]) => model);
77
+ (0, vitest_1.expect)(modelsCalled).toEqual(['smart-model-mock', 'fast-model-mock', 'fast-model-mock']);
78
+ });
79
+ });
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.aiClient = exports.AIClient = void 0;
7
7
  exports.getDefaultModel = getDefaultModel;
8
+ exports.modelSupportsTemperature = modelSupportsTemperature;
8
9
  exports.getMaxMessageContentLength = getMaxMessageContentLength;
9
10
  exports.getMaxHistoryLength = getMaxHistoryLength;
10
11
  exports.getContextWindowSize = getContextWindowSize;
@@ -17,6 +18,12 @@ const config_1 = require("./config");
17
18
  // Default model mapping
18
19
  // ---------------------------------------------------------------------------
19
20
  const DEFAULT_MODELS = {
21
+ // Smart-tier picks track each provider's current flagship for
22
+ // reasoning/coding workloads. Update here when a newer model becomes
23
+ // generally available so both the feature routes and the agent server pick
24
+ // it up automatically. When swapping a smart model in, also verify whether
25
+ // it accepts the `temperature` parameter and update
26
+ // `modelSupportsTemperature` accordingly.
20
27
  openai: { fast: 'gpt-4o-mini', smart: 'gpt-5.5' },
21
28
  gemini: { fast: 'gemini-2.5-flash', smart: 'gemini-2.5-pro' },
22
29
  anthropic: { fast: 'claude-haiku-4-5-20251001', smart: 'claude-opus-4-7' },
@@ -24,6 +31,39 @@ const DEFAULT_MODELS = {
24
31
  function getDefaultModel(provider, tier) {
25
32
  return DEFAULT_MODELS[provider][tier];
26
33
  }
34
+ /**
35
+ * Returns whether a given model accepts the `temperature` parameter.
36
+ *
37
+ * Provider-specific rules (validated against published API docs and SDKs as
38
+ * of late 2025 / early 2026):
39
+ * - OpenAI GPT-5 family (`gpt-5`, `gpt-5-mini`, `gpt-5.1`, …): NOT supported.
40
+ * The API only accepts the default value (1) and returns
41
+ * `unsupported_value: 'temperature'` for anything else.
42
+ * - OpenAI o-series reasoning models (`o1`, `o3`, `o4-mini`, …): NOT
43
+ * supported for the same reason.
44
+ * - OpenAI GPT-4 / GPT-4o / GPT-3.5: supported.
45
+ * - Google Gemini (2.x and 3.x families): supported via `generationConfig`.
46
+ * - Anthropic Claude (Sonnet, Haiku, and Opus 4.x): supported, with the
47
+ * exception of `claude-opus-4-7` (and its dated revisions) which rejects
48
+ * `temperature` just like the OpenAI GPT-5 family.
49
+ */
50
+ function modelSupportsTemperature(model) {
51
+ // OpenAI GPT-5 family (gpt-5, gpt-5-mini, gpt-5.1, gpt-5.5, …) only
52
+ // accepts the default temperature (1) — anything else is rejected with
53
+ // `unsupported_value: 'temperature'`.
54
+ if (/^gpt-5(\b|[.\-])/i.test(model))
55
+ return false;
56
+ // OpenAI o-series reasoning models (o1, o3, o4-mini, …) likewise drop the
57
+ // `temperature` knob.
58
+ if (/^o[134](\b|[-_])/i.test(model))
59
+ return false;
60
+ // Anthropic's Claude Opus 4.7 line (and its dated revisions like
61
+ // `claude-opus-4-7-20260101`) does not accept `temperature`; the rest of
62
+ // the Claude 4.x family (Sonnet, Haiku, Opus 4.5/4.6) does.
63
+ if (/^claude-opus-4-7(\b|[-_])/i.test(model))
64
+ return false;
65
+ return true;
66
+ }
27
67
  /**
28
68
  * Maximum character length for a single message content string per provider.
29
69
  *
@@ -88,7 +128,7 @@ class OpenAIAdapter {
88
128
  model,
89
129
  messages: oaiMessages,
90
130
  tools: tools?.length ? tools : undefined,
91
- temperature: model === 'gpt-5.5' ? 1 : (options.temperature ?? 0.2),
131
+ ...(modelSupportsTemperature(model) ? { temperature: options.temperature ?? 0.2 } : {}),
92
132
  max_tokens: options.maxTokens,
93
133
  });
94
134
  const choice = completion.choices[0];
@@ -125,7 +165,9 @@ class OpenAIAdapter {
125
165
  const stream = await this.client.chat.completions.create({
126
166
  model,
127
167
  messages: oaiMessages,
128
- temperature: options.temperature ?? 0.3,
168
+ ...(modelSupportsTemperature(model)
169
+ ? { temperature: options.temperature ?? 0.3 }
170
+ : {}),
129
171
  stream: true,
130
172
  stream_options: { include_usage: true },
131
173
  });
@@ -188,7 +230,7 @@ class AnthropicAdapter {
188
230
  ...(system ? { system } : {}),
189
231
  messages: anthropicMessages,
190
232
  ...(tools?.length ? { tools } : {}),
191
- ...(model === 'claude-opus-4-7' ? {} : { temperature: options.temperature ?? 0.2 }),
233
+ ...(modelSupportsTemperature(model) ? { temperature: options.temperature ?? 0.2 } : {}),
192
234
  });
193
235
  const textContent = response.content
194
236
  .filter((b) => b.type === 'text')
@@ -235,7 +277,9 @@ class AnthropicAdapter {
235
277
  max_tokens: options.maxTokens ?? 8192,
236
278
  ...(system ? { system } : {}),
237
279
  messages: anthropicMessages,
238
- temperature: options.temperature ?? 0.3,
280
+ ...(modelSupportsTemperature(model)
281
+ ? { temperature: options.temperature ?? 0.3 }
282
+ : {}),
239
283
  });
240
284
  for await (const event of stream) {
241
285
  if (event.type === 'content_block_delta' &&
@@ -269,7 +313,9 @@ class GeminiAdapter {
269
313
  config: {
270
314
  ...(systemInstruction ? { systemInstruction } : {}),
271
315
  ...(tools?.length ? { tools } : {}),
272
- temperature: options.temperature ?? 0.2,
316
+ ...(modelSupportsTemperature(model)
317
+ ? { temperature: options.temperature ?? 0.2 }
318
+ : {}),
273
319
  },
274
320
  });
275
321
  const candidate = response.candidates?.[0];
@@ -320,7 +366,9 @@ class GeminiAdapter {
320
366
  contents,
321
367
  config: {
322
368
  ...(systemInstruction ? { systemInstruction } : {}),
323
- temperature: options.temperature ?? 0.3,
369
+ ...(modelSupportsTemperature(model)
370
+ ? { temperature: options.temperature ?? 0.3 }
371
+ : {}),
324
372
  },
325
373
  });
326
374
  let usage;
@@ -56,13 +56,12 @@ async function getPromptForCommand(logger, cmd, subscription) {
56
56
  return '';
57
57
  }
58
58
  function getModelForCommand(cmd) {
59
+ // 'task' is the custom-task command and routes to the smart-tier model.
60
+ // 'enhance' and 'grammar' use the fast tier. The actual model strings live
61
+ // in ai-client.ts (DEFAULT_MODELS) so all callers stay in sync when we
62
+ // upgrade to a newer flagship model.
59
63
  const tier = cmd === 'task' ? 'smart' : 'fast';
60
- const models = {
61
- openai: { fast: 'gpt-4o-mini', smart: 'gpt-5.5' },
62
- gemini: { fast: 'gemini-2.5-flash', smart: 'gemini-2.5-pro' },
63
- anthropic: { fast: 'claude-haiku-4-5-20251001', smart: 'claude-opus-4-7' },
64
- };
65
- return models[config_1.config.aiProvider]?.[tier] ?? 'gpt-4o-mini';
64
+ return (0, ai_client_1.getDefaultModel)(config_1.config.aiProvider, tier);
66
65
  }
67
66
  function createMessagesParams(cmd, input, prompt) {
68
67
  if (cmd === 'task') {
@@ -100,7 +99,14 @@ async function runEnhancementModel(logger, text, cmd, subscription, onDelta) {
100
99
  const messages = createMessagesParams(cmd, trimmed, prompt);
101
100
  let rawResponse = '';
102
101
  let usage;
103
- const result = await ai_client_1.aiClient.streamComplete(model, messages, { temperature: 0.3 }, (delta) => {
102
+ // Smart-tier models (used by the custom-task command) include OpenAI's
103
+ // GPT-5 family, which rejects any non-default `temperature`. Even on
104
+ // providers where the smart model still accepts it (Gemini, Anthropic),
105
+ // omitting `temperature` keeps the request shape uniform across providers
106
+ // and lets each model use its own tuned default. The fast-tier models used
107
+ // by `enhance` and `grammar` keep the previous 0.3 default.
108
+ const completionOptions = cmd === 'task' ? {} : { temperature: 0.3 };
109
+ const result = await ai_client_1.aiClient.streamComplete(model, messages, completionOptions, (delta) => {
104
110
  rawResponse += delta;
105
111
  if (onDelta)
106
112
  onDelta(delta);
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public",
5
5
  "registry": "https://registry.npmjs.org/"
6
6
  },
7
- "version": "1.2.0",
7
+ "version": "1.3.0",
8
8
  "description": "CLI for onboarding users to Omnikey AI and configuring OPENAI_API_KEY. Use Yarn for install/build.",
9
9
  "engines": {
10
10
  "node": ">=14.0.0",