testronaut 1.4.0 → 1.5.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/README.md CHANGED
@@ -134,7 +134,7 @@ Current supported providers:
134
134
 
135
135
  | Provider | Example Models |
136
136
  |-----------|----------------|
137
- | **OpenAI** | gpt-4o, gpt-4.1, o3, gpt-5, gpt-5.1, etc. |
137
+ | **OpenAI** | gpt-5.6, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5, and legacy GPT/o-series models |
138
138
  | **Google Gemini** | gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-8b |
139
139
 
140
140
  More providers coming soon (Anthropic, Mistral, etc.).
package/bin/init.js CHANGED
@@ -32,10 +32,10 @@ import {
32
32
  migrateLegacyConfig,
33
33
  defaultConfig,
34
34
  makeEnvTemplate,
35
- openAIModels,
36
35
  geminiModels,
37
36
  pickInitialIndex
38
37
  } from './initHelpers.js';
38
+ import { DEFAULT_OPENAI_MODEL, OPENAI_MODELS } from '../llm/openAI/models.js';
39
39
 
40
40
  export async function initializeTestronautProject() {
41
41
  // ─────────────────────────────────────────────
@@ -81,36 +81,36 @@ export async function initializeTestronautProject() {
81
81
 
82
82
  // 2b) Provider-specific model selection
83
83
  if (llmProvider === 'openai') {
84
- const models = openAIModels();
84
+ const currentModels = OPENAI_MODELS.filter(model => !model.legacy);
85
+ const legacyModels = OPENAI_MODELS.filter(model => model.legacy);
86
+ const choices = [
87
+ ...currentModels.map(model => ({
88
+ title: `${model.label} (${model.description})`,
89
+ value: model.id,
90
+ })),
91
+ { title: '── Legacy models ──', disabled: true },
92
+ ...legacyModels.map(model => ({
93
+ title: `${model.label} (${model.description})`,
94
+ value: model.id,
95
+ })),
96
+ ];
97
+ const selectedIndex = choices.findIndex(choice => choice.value === config.model);
98
+ const defaultIndex = choices.findIndex(choice => choice.value === DEFAULT_OPENAI_MODEL);
85
99
  const { openaiModel } = await prompts({
86
100
  type: 'select',
87
101
  name: 'openaiModel',
88
102
  message: 'Select an OpenAI model for agentic workflows (function/tool calling):',
89
- choices: [
90
- { title: 'GPT-4.1 (general purpose)', value: 'gpt-4.1' },
91
- { title: 'GPT-4.1 mini (faster, cheaper)', value: 'gpt-4.1-mini' },
92
- { title: 'GPT-4o (multimodal, tool use)', value: 'gpt-4o' },
93
- { title: 'GPT-4o mini (speed/cost optimized)', value: 'gpt-4o-mini' },
94
- { title: 'o3 (reasoning w/ native tool use)', value: 'o3' },
95
- { title: 'o4-mini (reasoning, cost-effective)', value: 'o4-mini' },
96
- // GPT-5 family (availability varies)
97
- { title: 'GPT-5 (previous reasoning model)', value: 'gpt-5' },
98
- { title: 'GPT-5 mini (faster)', value: 'gpt-5-mini' },
99
- { title: 'GPT-5 nano (lightweight)', value: 'gpt-5-nano' },
100
- { title: 'GPT-5.1 (latest reasoning model)', value: 'gpt-5.1' },
101
- ],
102
- // Keep prior selection highlighted if present; fallback to 4.1-mini.
103
- initial: pickInitialIndex(models, config.model, 'gpt-4.1-mini')
103
+ choices,
104
+ // Keep a prior selection highlighted; otherwise default to GPT-5.6.
105
+ initial: selectedIndex >= 0 ? selectedIndex : defaultIndex,
104
106
  });
105
107
 
106
108
  config.model = openaiModel;
107
109
 
108
- // Helpful heads-up: GPT-5 access varies by account/region.
110
+ // API access and rate limits vary by OpenAI account and project.
109
111
  if (openaiModel?.startsWith('gpt-5')) {
110
112
  console.log(`
111
- ⚠️ Warning: GPT-5 support may not be available for all users.
112
- - Access depends on your OpenAI account/region
113
- - Some capabilities may be gated or rate-limited
113
+ ℹ️ GPT-5 availability and rate limits depend on your OpenAI account and project.
114
114
  `);
115
115
  }
116
116
  } else if (llmProvider === 'gemini') {
@@ -25,6 +25,8 @@
25
25
  * const models = openAIModels();
26
26
  */
27
27
 
28
+ import { OPENAI_MODELS } from '../llm/openAI/models.js';
29
+
28
30
  /**
29
31
  * Ensures backward compatibility for older configs where
30
32
  * provider information may have been implied by the model field.
@@ -94,18 +96,7 @@ GEMINI_API_KEY=AIza...
94
96
  * @returns {string[]} Array of OpenAI model names
95
97
  */
96
98
  export function openAIModels() {
97
- return [
98
- 'gpt-4.1',
99
- 'gpt-4.1-mini',
100
- 'gpt-4o',
101
- 'gpt-4o-mini',
102
- 'o3',
103
- 'o4-mini',
104
- 'gpt-5',
105
- 'gpt-5-mini',
106
- 'gpt-5-nano',
107
- 'gpt-5.1',
108
- ];
99
+ return OPENAI_MODELS.map(({ id }) => id);
109
100
  }
110
101
 
111
102
  /**
package/core/turnLoop.js CHANGED
@@ -41,7 +41,8 @@ import {
41
41
  tokenUseCoolOff,
42
42
  recordTokenUsage,
43
43
  pruneOldTokenUsage,
44
- updateLimitsFromHeaders
44
+ updateLimitsFromHeaders,
45
+ warnIfContextNearLimit
45
46
  } from '../tools/tokenControl.js';
46
47
  import { resolveProviderModel } from '../llm/modelResolver.js';
47
48
  import { getLLM } from '../llm/llmFactory.js';
@@ -349,6 +350,7 @@ export const turnLoop = async (
349
350
  // ─────────────────────────────────────────────
350
351
  // STEP 2: Request next reasoning turn from model
351
352
  // ─────────────────────────────────────────────
353
+ await warnIfContextNearLimit(MODEL_ID, { messages, tools: activeToolsSchema });
352
354
  const { message, usage, headers } = await llm.chat({
353
355
  model: MODEL_ID,
354
356
  messages,
@@ -9,7 +9,7 @@
9
9
  * 1) Env: TESTRONAUT_PROVIDER / TESTRONAUT_MODEL
10
10
  * 2) Config file (provider + model)
11
11
  * 3) Legacy config (model === "openai", no provider)
12
- * 4) Default: { provider: "openai", model: "gpt-4o" }
12
+ * 4) Default: { provider: "openai", model: "gpt-5.6" }
13
13
  *
14
14
  * Related tests: tests/llmTests/modelResolver.test.js
15
15
  * Used by: core/turnLoop.js, llm/llmFactory.js
@@ -17,6 +17,7 @@
17
17
 
18
18
  import path from 'path';
19
19
  import fs from 'fs';
20
+ import { DEFAULT_OPENAI_MODEL } from './openAI/models.js';
20
21
 
21
22
  /**
22
23
  * Resolve provider & model, honoring env overrides and safe defaults.
@@ -50,7 +51,7 @@ export function resolveProviderModel(opts = {}) {
50
51
 
51
52
  // Legacy: model === "openai" and no provider
52
53
  if (!cfg?.provider && cfg?.model === 'openai') {
53
- return { provider: 'openai', model: envModel || 'gpt-4o' };
54
+ return { provider: 'openai', model: envModel || DEFAULT_OPENAI_MODEL };
54
55
  }
55
56
  }
56
57
  } catch (e) {
@@ -60,6 +61,6 @@ export function resolveProviderModel(opts = {}) {
60
61
  // 3) Defaults
61
62
  return {
62
63
  provider: envProvider || 'openai',
63
- model: envModel || 'gpt-4o',
64
+ model: envModel || DEFAULT_OPENAI_MODEL,
64
65
  };
65
66
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Supported OpenAI model aliases and the capabilities Testronaut relies on.
3
+ * Dated snapshots and specialized Chat, Codex, Pro, and Cyber models are
4
+ * intentionally excluded from the interactive catalog.
5
+ */
6
+ export const OPENAI_MODELS = [
7
+ { id: 'gpt-5.6', label: 'GPT-5.6 Sol', description: 'flagship capability', contextWindow: 1050000, chatToolReasoningEffort: 'none' },
8
+ { id: 'gpt-5.6-terra', label: 'GPT-5.6 Terra', description: 'balanced intelligence and cost', contextWindow: 1050000, chatToolReasoningEffort: 'none' },
9
+ { id: 'gpt-5.6-luna', label: 'GPT-5.6 Luna', description: 'cost-sensitive, high-volume workloads', contextWindow: 1050000, chatToolReasoningEffort: 'none' },
10
+ { id: 'gpt-5.5', label: 'GPT-5.5', description: 'previous flagship model', contextWindow: 1050000 },
11
+
12
+ { id: 'gpt-5.4', label: 'GPT-5.4', description: 'earlier professional model', contextWindow: 1050000, legacy: true },
13
+ { id: 'gpt-5.4-mini', label: 'GPT-5.4 mini', description: 'earlier balanced model', contextWindow: 400000, legacy: true },
14
+ { id: 'gpt-5.4-nano', label: 'GPT-5.4 nano', description: 'earlier high-volume model', contextWindow: 400000, legacy: true },
15
+ { id: 'gpt-5.2', label: 'GPT-5.2', description: 'previous frontier model', contextWindow: 400000, legacy: true },
16
+ { id: 'gpt-5.1', label: 'GPT-5.1', description: 'legacy reasoning model', legacy: true },
17
+ { id: 'gpt-5', label: 'GPT-5', description: 'legacy reasoning model', legacy: true },
18
+ { id: 'gpt-5-mini', label: 'GPT-5 mini', description: 'legacy faster model', legacy: true },
19
+ { id: 'gpt-5-nano', label: 'GPT-5 nano', description: 'legacy lightweight model', legacy: true },
20
+ { id: 'gpt-4.1', label: 'GPT-4.1', description: 'legacy general-purpose model', legacy: true },
21
+ { id: 'gpt-4.1-mini', label: 'GPT-4.1 mini', description: 'legacy faster model', legacy: true },
22
+ { id: 'gpt-4o', label: 'GPT-4o', description: 'legacy multimodal model', legacy: true },
23
+ { id: 'gpt-4o-mini', label: 'GPT-4o mini', description: 'legacy cost-optimized model', legacy: true },
24
+ { id: 'o3', label: 'o3', description: 'legacy reasoning model', legacy: true },
25
+ { id: 'o4-mini', label: 'o4-mini', description: 'legacy cost-effective reasoning model', legacy: true },
26
+ ];
27
+
28
+ export const DEFAULT_OPENAI_MODEL = 'gpt-5.6';
29
+
30
+ export function getOpenAIModel(modelId) {
31
+ return OPENAI_MODELS.find(({ id }) => id === modelId);
32
+ }
@@ -20,6 +20,7 @@
20
20
  */
21
21
 
22
22
  import OpenAI from 'openai';
23
+ import { getOpenAIModel } from './models.js';
23
24
 
24
25
  export class OpenAIProvider {
25
26
  constructor({ apiKey } = {}) {
@@ -33,11 +34,21 @@ export class OpenAIProvider {
33
34
  * @returns {Promise<{message:any, usage?:{total_tokens?:number, providerRaw?:any}, headers?:any}>}
34
35
  */
35
36
  async chat({ model, messages, tools }) {
36
- const res = await this.client.chat.completions.create({
37
+ const request = {
37
38
  model,
38
39
  messages,
39
40
  tools,
40
- });
41
+ };
42
+
43
+ // GPT-5.6 defaults to medium reasoning, but Chat Completions does not
44
+ // support combining reasoning with function tools. Responses API support
45
+ // is tracked separately; keep the current tool loop compatible meanwhile.
46
+ const chatToolReasoningEffort = getOpenAIModel(model)?.chatToolReasoningEffort;
47
+ if (tools?.length && chatToolReasoningEffort) {
48
+ request.reasoning_effort = chatToolReasoningEffort;
49
+ }
50
+
51
+ const res = await this.client.chat.completions.create(request);
41
52
 
42
53
  // OpenAI already returns an OpenAI-like message and usage structure.
43
54
  const message = res.choices?.[0]?.message ?? { role: 'assistant', content: '' };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testronaut",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "main": "./runner/testronaut.js",
5
5
  "bin": {
6
6
  "testronaut": "bin/cli.js"
@@ -31,7 +31,7 @@
31
31
  "fast-glob": "^3.3.3",
32
32
  "inquirer": "^12.9.1",
33
33
  "node-fetch": "^3.3.2",
34
- "openai": "^5.10.2",
34
+ "openai": "^6.39.0",
35
35
  "playwright": "^1.54.1",
36
36
  "prompts": "^2.4.2"
37
37
  },
@@ -43,7 +43,7 @@
43
43
  "qa"
44
44
  ],
45
45
  "devDependencies": {
46
- "@types/node": "^24.9.1",
46
+ "@types/node": "^25.9.3",
47
47
  "periapsis": "^2.0.1",
48
48
  "vitest": "^4.1.9"
49
49
  }
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "prCheck": {
21
21
  "enabled": true,
22
- "failOnBreach": true,
22
+ "failOnBreach": false,
23
23
  "advisoryOnly": false,
24
24
  "minimumSeverity": "high"
25
25
  },
@@ -4,6 +4,17 @@ import { openAIModels, geminiModels, isKnownModel, pickInitialIndex } from '../.
4
4
  describe('model helpers', () => {
5
5
  it('lists known models', () => {
6
6
  expect(openAIModels()).toContain('gpt-4o');
7
+ expect(openAIModels()).toEqual(expect.arrayContaining([
8
+ 'gpt-5.2',
9
+ 'gpt-5.4',
10
+ 'gpt-5.4-mini',
11
+ 'gpt-5.4-nano',
12
+ 'gpt-5.5',
13
+ 'gpt-5.6',
14
+ 'gpt-5.6-terra',
15
+ 'gpt-5.6-luna',
16
+ ]));
17
+ expect(openAIModels()).not.toContain('gpt-5.3');
7
18
  expect(geminiModels()).toContain('gemini-2.5-flash');
8
19
  });
9
20
 
@@ -67,6 +67,7 @@ vi.mock('../../tools/tokenControl.js', () => ({
67
67
  totalTokensUsed: turnTimestamps.reduce((a, [, t]) => a + t, 0),
68
68
  })),
69
69
  updateLimitsFromHeaders: vi.fn(() => {}),
70
+ warnIfContextNearLimit: vi.fn(async () => ({ warned: false })),
70
71
  }));
71
72
 
72
73
  vi.mock('../../tools/toolSchema.js', () => ({
@@ -62,13 +62,13 @@ describe('resolveProviderModel', () => {
62
62
  writeConfig(temp, { model: 'openai' });
63
63
 
64
64
  const res = resolveProviderModel({ cwd: temp });
65
- expect(res).toEqual({ provider: 'openai', model: 'gpt-4o' });
65
+ expect(res).toEqual({ provider: 'openai', model: 'gpt-5.6' });
66
66
  });
67
67
 
68
68
  it('returns defaults when config missing', () => {
69
69
  const temp = makeTempProject();
70
70
  const res = resolveProviderModel({ cwd: temp });
71
- expect(res).toEqual({ provider: 'openai', model: 'gpt-4o' });
71
+ expect(res).toEqual({ provider: 'openai', model: 'gpt-5.6' });
72
72
  });
73
73
 
74
74
  it('returns defaults when JSON parsing fails', () => {
@@ -77,7 +77,7 @@ describe('resolveProviderModel', () => {
77
77
 
78
78
  const res = resolveProviderModel({ cwd: temp });
79
79
  expect(res.provider).toBe('openai');
80
- expect(res.model).toBe('gpt-4o');
80
+ expect(res.model).toBe('gpt-5.6');
81
81
  });
82
82
 
83
83
  it('trims env vars and treats empty strings as absent', () => {
@@ -90,7 +90,7 @@ describe('resolveProviderModel', () => {
90
90
  process.env.TESTRONAUT_MODEL = ' ';
91
91
  res = resolveProviderModel();
92
92
  // falls back to defaults when env vars are just whitespace
93
- expect(res).toEqual({ provider: 'openai', model: 'gpt-4o' });
93
+ expect(res).toEqual({ provider: 'openai', model: 'gpt-5.6' });
94
94
  });
95
95
 
96
96
  it('prefers config when only provider is in env and model only in config', () => {
@@ -78,6 +78,24 @@ describe('OpenAIProvider', () => {
78
78
  });
79
79
  });
80
80
 
81
+ it.each(['gpt-5.6', 'gpt-5.6-terra', 'gpt-5.6-luna'])(
82
+ 'disables reasoning for %s function tools on Chat Completions',
83
+ async (model) => {
84
+ const prov = new OpenAIProvider({ apiKey: 'sk-abc' });
85
+ const tools = [{ type: 'function', function: { name: 'doThing', parameters: { type: 'object' } } }];
86
+
87
+ await prov.chat({ model, messages: [{ role: 'user', content: 'go' }], tools });
88
+
89
+ expect(shared.lastCreateArgs).toMatchObject({ model, tools, reasoning_effort: 'none' });
90
+ }
91
+ );
92
+
93
+ it('does not add a reasoning override to GPT-5.6 requests without tools', async () => {
94
+ const prov = new OpenAIProvider({ apiKey: 'sk-abc' });
95
+ await prov.chat({ model: 'gpt-5.6', messages: [] });
96
+ expect(shared.lastCreateArgs).not.toHaveProperty('reasoning_effort');
97
+ });
98
+
81
99
  it('returns normalized message, usage, and headers', async () => {
82
100
  const prov = new OpenAIProvider({ apiKey: 'sk-abc' });
83
101
  const { message, usage, headers } = await prov.chat({
@@ -33,6 +33,7 @@ import {
33
33
  tokenUseCoolOff,
34
34
  recordTokenUsage,
35
35
  pruneOldTokenUsage,
36
+ warnIfContextNearLimit,
36
37
  __resetTokenControlForTests,
37
38
  } from '../../tools/tokenControl.js';
38
39
 
@@ -111,6 +112,24 @@ describe('tokenControl', () => {
111
112
  // sanity check it actually changed (unless defaults already 1234)
112
113
  if (before.tpm !== 1234) expect(after.tpm).not.toBe(before.tpm);
113
114
  });
115
+
116
+ it('uses tier-1 defaults without letting generic GPT-5 rules shadow variants', () => {
117
+ expect(getCurrentTokenLimit('gpt-5.6').tpm).toBe(500000);
118
+ expect(getCurrentTokenLimit('gpt-5.6-luna').tpm).toBe(500000);
119
+ expect(getCurrentTokenLimit('gpt-5.4-nano').tpm).toBe(200000);
120
+ expect(getCurrentTokenLimit('gpt-5-mini').tpm).toBe(240000);
121
+ expect(getCurrentTokenLimit('gpt-5-nano').tpm).toBe(600000);
122
+ });
123
+ });
124
+
125
+ it('warns without truncating when known context usage crosses the threshold', async () => {
126
+ tiktokenMocks.encoding_for_model_impl.mockReturnValue(encMock);
127
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
128
+ const result = await warnIfContextNearLimit('gpt-5.2', '1234567890', 0.00002);
129
+ expect(result.warned).toBe(true);
130
+ expect(result.contextWindow).toBe(400000);
131
+ expect(warn).toHaveBeenCalled();
132
+ warn.mockRestore();
114
133
  });
115
134
 
116
135
  describe('rolling window + cooldown', () => {
package/todo.md ADDED
@@ -0,0 +1,17 @@
1
+ # Testronaut CLI TODO
2
+
3
+ ## OpenAI provider follow-ups
4
+
5
+ - Migrate the OpenAI adapter from Chat Completions to the Responses API in a
6
+ dedicated change. Preserve Testronaut's provider-neutral message and tool-call
7
+ contract, add response-ID/reasoning-state handling, normalize usage and raw
8
+ headers, and cover multi-turn tool continuation with unit and API smoke tests.
9
+ Once migrated, remove the GPT-5.6 Chat Completions compatibility override that
10
+ sets `reasoning_effort` to `none` for function-tool requests.
11
+ - Add active context budgeting after its behavior is designed and tested. Use
12
+ model context/output metadata to reserve response and reasoning capacity, then
13
+ apply explicit history compaction or summarization rules. Do not silently
14
+ truncate mission instructions, current DOM state, or unmatched tool calls.
15
+ - Learn effective OpenAI TPM/RPM limits from successful response headers and
16
+ rate-limit errors. Prefer observed project/model limits over inferring a named
17
+ account tier; retain the environment override and conservative startup values.
@@ -22,6 +22,7 @@
22
22
 
23
23
  import { encoding_for_model, get_encoding } from '@dqbd/tiktoken';
24
24
  import { wait } from './turnLoopUtils.js';
25
+ import { getOpenAIModel } from '../llm/openAI/models.js';
25
26
 
26
27
  /**
27
28
  * Dynamic token-per-minute limits by model family.
@@ -35,10 +36,16 @@ import { wait } from './turnLoopUtils.js';
35
36
  */
36
37
  const DEFAULT_LIMITS = [
37
38
  // ── OpenAI (newer first) ────────────────────────────────────────────────
39
+ { test: /^gpt-5\.6-luna(-|$)/i, tpm: 500000 },
40
+ { test: /^gpt-5\.6(-|$)/i, tpm: 500000 },
41
+ { test: /^gpt-5\.5(-|$)/i, tpm: 500000 },
42
+ { test: /^gpt-5\.4-nano(-|$)/i, tpm: 200000 },
43
+ { test: /^gpt-5\.4(-|$)/i, tpm: 500000 },
44
+ { test: /^gpt-5\.2(-|$)/i, tpm: 500000 },
38
45
  { test: /^gpt-5.1(-|$)/i, tpm: 120000 },
39
- { test: /^gpt-5(-|$)/i, tpm: 90000 },
40
46
  { test: /^gpt-5-mini(-|$)/i, tpm: 240000 },
41
47
  { test: /^gpt-5-nano(-|$)/i, tpm: 600000 },
48
+ { test: /^gpt-5(-|$)/i, tpm: 90000 },
42
49
 
43
50
  { test: /^gpt-4o(-|$)/i, tpm: 450000 },
44
51
  { test: /^gpt-4\.1(-|$)/i, tpm:1000000 },
@@ -143,6 +150,25 @@ export const tokenEstimate = async (model, text) => {
143
150
  return approx;
144
151
  };
145
152
 
153
+ /**
154
+ * Warn when a request is close to a known model context window. This is a
155
+ * diagnostic only: Testronaut does not truncate or compact content here.
156
+ */
157
+ export async function warnIfContextNearLimit(model, payload, threshold = 0.9) {
158
+ const contextWindow = getOpenAIModel(model)?.contextWindow;
159
+ if (!contextWindow) return { warned: false };
160
+
161
+ const estimatedTokens = await tokenEstimate(model, payload);
162
+ const warned = estimatedTokens >= contextWindow * threshold;
163
+ if (warned) {
164
+ console.warn(
165
+ `⚠️ Estimated request context for ${model} is ${estimatedTokens}/${contextWindow} tokens. ` +
166
+ 'Testronaut will send it unchanged; consider reducing mission or tool history.'
167
+ );
168
+ }
169
+ return { warned, estimatedTokens, contextWindow };
170
+ }
171
+
146
172
  /* ---------------- Dynamic limit resolution ---------------- */
147
173
 
148
174
  /**