Skip to content

Commit 179eb03

Browse files
authored
Merge pull request #108 from levelcodeai/feat/gemini-4-argon
feat(ai): Gemini 4 Argon, sized as it is — its window, its reasoning, and room to think
2 parents 0f6a0dc + c6bf7ba commit 179eb03

10 files changed

Lines changed: 283 additions & 10 deletions

File tree

‎extensions/levelcode-ai/agent.js‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const cp = require('child_process');
1616
const os = require('os'); // MCP servers need a cwd even when no folder is open
1717
const host = require('./host'); // the machine we run on: workspace I/O is async here, and capabilities differ (browser)
1818
const providers = require('./providers/index');
19+
const catalog = require('./providers/catalog');
1920
const { formatVerifyFeedback, verifyOutcome, looksUnrunnable, sniffPort, sniffPreviewUrl, looksReady } = require('./verify');
2021
const { classifyCommand, dangerLabel } = require('./commandSafety');
2122
const { commitShellChanges } = require('./shellCommit');
@@ -1142,7 +1143,11 @@ async function runAgent(ctx) {
11421143
// full API call whose input grows with the (never-truncated) transcript, so an adversarial or
11431144
// runaway model that keeps hitting the cap could otherwise rack up unbounded, user-billed calls.
11441145
// Budget = a generous multiple of the per-turn cap; once exceeded we stop continuing and hand back.
1145-
const perTurnMax = Math.max(1024, Math.min(32768, ctx.maxTokens || 8192));
1146+
// A reasoning model spends its hidden reasoning out of the same max_tokens, so the 8,192 default can
1147+
// leave nothing for the answer (Gemini 4 Argon reasons on every turn). Such models get the ceiling,
1148+
// 32,768, which is also the most a paid request may ask of the LevelCode gateway.
1149+
const reasons = !!catalog.modelCaps(ctx.model).reasoning;
1150+
const perTurnMax = Math.max(1024, Math.min(32768, reasons ? 32768 : (ctx.maxTokens || 8192)));
11461151
const OUTPUT_TOKEN_BUDGET = perTurnMax * 8;
11471152
let cumulativeOutputTokens = 0;
11481153
// [LevelCode] Gateway turns report real money: this run's cost + the wallet balance, both RETAIL
@@ -1219,6 +1224,7 @@ async function runAgent(ctx) {
12191224
const turnOpts = {
12201225
providerId: ctx.providerId, baseURL: ctx.baseURL, label: ctx.label,
12211226
apiKey: ctx.apiKey, model: ctx.model, maxTokens: perTurnMax, system: system,
1227+
reasoningEffort: catalog.reasoningEffortFor(ctx.model, 'agent'),
12221228
messages, tools: tools, signal: ctx.signal,
12231229
onText: (t) => { streamed = true; textChars += t.length; ctx.post({ type: 'agentDelta', text: t }); },
12241230
onToolStart: (name, id) => {

‎extensions/levelcode-ai/extension.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2159,7 +2159,10 @@ async function compactAgentMemory() {
21592159
// than Compact failing on a 401 that the next chat message would have recovered from.
21602160
summary = await authRetry(req, (r) => providers.complete({
21612161
providerId: r.providerId, apiKey: r.apiKey, baseURL: r.baseURL, label: r.label,
2162-
model: r.model, maxTokens: 1500,
2162+
// A reasoning model thinks out of the same budget, and 1,500 tokens of thought leaves an empty
2163+
// summary. It gets room for both, and is asked to think little.
2164+
model: r.model, maxTokens: catalog.modelCaps(r.model).reasoning ? 8192 : 1500,
2165+
reasoningEffort: catalog.reasoningEffortFor(r.model, 'compact'),
21632166
system: COMPACT_SYSTEM,
21642167
messages: [{ role: 'user', content: COMPACT_INSTRUCTIONS + flat }]
21652168
}));
@@ -2586,6 +2589,7 @@ async function handleSend(text, images) {
25862589
const doStream = (r) => providers.streamChat({
25872590
providerId: r.providerId, apiKey: r.apiKey, baseURL: r.baseURL, label: r.label,
25882591
model: r.model, maxTokens: r.maxTokens, system: SYSTEM_PROMPT,
2592+
reasoningEffort: catalog.reasoningEffortFor(r.model, 'chat'),
25892593
messages: withImages(conversation), signal: abort.signal, onDelta
25902594
});
25912595
try {

‎extensions/levelcode-ai/providers/catalog.js‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ const CAPS = {
4242
// real one is 1.05M (OpenRouter models API, 2026-09-06) and it reads images. Without this row the
4343
// gateway picker offers it while the composer refuses attachments and the meter sizes it wrong.
4444
'gpt-6-astra': { context: 1050000, tools: true, vision: true },
45+
// Gemini 4 Argon (2026-10-13): a 1,048,576 window, image and file input, and reasoning that is always
46+
// on. The `gemini` heuristic below gave it 1,000,000 and no reasoning flag. No `fast`: it must never
47+
// become the ghost-text model. `effort` is sent as `reasoning: { effort }` (see reasoningEffortFor),
48+
// and only under the vendor-prefixed id, which is how OpenRouter and the LevelCode gateway name it;
49+
// both take that field for this model. A bare id may reach some other endpoint, so it asks for none.
50+
'google/gemini-4-argon': { context: 1048576, tools: true, vision: true, reasoning: true, effort: 'medium' },
51+
'gemini-4-argon': { context: 1048576, tools: true, vision: true, reasoning: true },
4552
'o3-mini': { context: 200000, tools: true, reasoning: true },
4653
'o1': { context: 200000, tools: true, reasoning: true },
4754
'o1-mini': { context: 128000, tools: false, reasoning: true }, // o1-mini has no function calling
@@ -191,6 +198,21 @@ function fastCompletionModel(providerId) {
191198
return (p && FAST_COMPLETION[p.id]) || null;
192199
}
193200

201+
/**
202+
* The `reasoning.effort` to ask of a model, or null to send none (the provider's default applies).
203+
* Only a model whose CAPS row names an effort gets one. `purpose` 'compact' is for the summaries the
204+
* editor writes for itself, which need little thought, so it asks for 'low'; chat and the agent get the
205+
* row's own effort.
206+
* @param {string} modelId
207+
* @param {'chat'|'agent'|'compact'} purpose
208+
* @returns {string|null}
209+
*/
210+
function reasoningEffortFor(modelId, purpose) {
211+
const effort = modelCaps(modelId).effort;
212+
if (!effort) { return null; }
213+
return purpose === 'compact' ? 'low' : effort;
214+
}
215+
194216
/** Format a caps object as a compact QuickPick detail string, e.g. "128k ctx · tools · vision". */
195217
function fmtContext(n) { return n >= 1000 ? Math.round(n / 1000) + 'k ctx' : n + ' ctx'; }
196218
function describeCaps(c) {
@@ -296,6 +318,7 @@ async function getModelChoices(providerId, opts) {
296318
module.exports = {
297319
CAPS, modelCaps, baseName, heuristicCaps,
298320
supportsToolsForModel, diagramSupportForModel, supportsVisionForModel, contextWindowFor, fastCompletionModel,
321+
reasoningEffortFor,
299322
describeCaps, describeModel,
300323
mapOpenRouterModels, mapModelIds, fetchModels, getModelChoices
301324
};

‎extensions/levelcode-ai/providers/index.js‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ async function streamChat(o) {
158158
return openai.streamOpenAI({
159159
baseURL: o.baseURL || p.baseURL, apiKey: o.apiKey, headers: p.headers, label: o.label || p.label,
160160
model: o.model, maxTokens: o.maxTokens, system: o.system, messages: o.messages,
161-
signal: o.signal, onDelta: o.onDelta, onRetry: o.onRetry
161+
signal: o.signal, onDelta: o.onDelta, onRetry: o.onRetry, reasoningEffort: o.reasoningEffort
162162
});
163163
}
164164

@@ -181,7 +181,7 @@ async function complete(o) {
181181
return openai.completeOpenAI({
182182
baseURL: o.baseURL || p.baseURL, apiKey: o.apiKey, headers: p.headers, label: o.label || p.label,
183183
model: o.model, maxTokens: o.maxTokens, system: o.system, messages: o.messages,
184-
stop: o.stop, signal: o.signal, onRetry: o.onRetry
184+
stop: o.stop, signal: o.signal, onRetry: o.onRetry, reasoningEffort: o.reasoningEffort
185185
});
186186
}
187187

@@ -216,7 +216,8 @@ async function streamAgentTurn(o) {
216216
return openai.streamOpenAIAgentTurn({
217217
baseURL: o.baseURL || p.baseURL, apiKey: o.apiKey, headers: p.headers, label: o.label || p.label,
218218
model: o.model, maxTokens: o.maxTokens, system: o.system, messages: o.messages, tools: o.tools,
219-
signal: o.signal, onText: o.onText, onToolStart: o.onToolStart, onToolInput: o.onToolInput, onRetry: o.onRetry
219+
signal: o.signal, onText: o.onText, onToolStart: o.onToolStart, onToolInput: o.onToolInput, onRetry: o.onRetry,
220+
reasoningEffort: o.reasoningEffort
220221
});
221222
}
222223

‎extensions/levelcode-ai/providers/openaiCompat.js‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@ function isReasoningModel(model) {
4747
* Build the /v1/chat/completions request body. Pure — unit-testable.
4848
* OpenAI has no top-level `system` field, so a `system` string becomes a leading
4949
* {role:'system'} message (exactly what the old Ollama path did). The output-token cap is sent as
50-
* `max_completion_tokens` for reasoning models and `max_tokens` otherwise.
50+
* `max_completion_tokens` for reasoning models and `max_tokens` otherwise. A `reasoningEffort` becomes
51+
* OpenRouter's `reasoning: { effort }`; callers pass one only for a model that takes it (catalog.js).
5152
* @param {{model:string, system?:string, messages:any[], maxTokens?:number,
52-
* stream?:boolean, stop?:string[], temperature?:number}} opts
53+
* stream?:boolean, stop?:string[], temperature?:number, reasoningEffort?:string|null}} opts
5354
*/
5455
function buildChatBody(opts) {
5556
const msgs = opts.system
@@ -62,6 +63,7 @@ function buildChatBody(opts) {
6263
if (opts.stream) { body.stream = true; }
6364
if (opts.stop && opts.stop.length) { body.stop = opts.stop; }
6465
if (opts.temperature != null && !reasoning) { body.temperature = opts.temperature; }
66+
if (opts.reasoningEffort) { body.reasoning = { effort: opts.reasoningEffort }; }
6567
return body;
6668
}
6769

@@ -262,7 +264,7 @@ async function streamOpenAIAgentTurn(opts) {
262264
// the field, so we gate it strictly on the model id. cached_tokens is still read for ALL providers below.
263265
const messages = translate.toOpenAIMessages(opts.system, opts.messages, { cache: isAnthropicFamily(opts.model) });
264266
const tools = translate.toOpenAITools(opts.tools);
265-
const body = buildChatBody({ model: opts.model, messages, maxTokens: opts.maxTokens, stream: true });
267+
const body = buildChatBody({ model: opts.model, messages, maxTokens: opts.maxTokens, stream: true, reasoningEffort: opts.reasoningEffort });
266268
if (tools) { body.tools = tools; }
267269
// Ask for a final usage chunk (choices:[] + usage) so the agent's context meter isn't stuck at 0
268270
// on OpenAI-shaped providers — they omit usage from streams unless include_usage is set. Mainstream
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* A reasoning model gets the whole per-turn output ceiling, and its effort — run: node test/agentReasoningBudget.test.js
3+
*
4+
* Gemini 4 Argon reasons on every turn, and its reasoning comes out of the same max_tokens as the answer.
5+
* At the 8,192 default a hard turn could spend it all thinking and stop with nothing said. A model whose
6+
* caps say it reasons now asks for 32,768 (the ceiling, and the LevelCode gateway's own limit for a paid
7+
* request), and a model whose row names an effort sends it. Everything else keeps the user's setting.
8+
*
9+
* This drives the real agent loop with a stand-in for the provider and reads what each turn asked for.
10+
*--------------------------------------------------------------------------------------------*/
11+
// @ts-check
12+
'use strict';
13+
14+
const assert = require('assert');
15+
const fs = require('fs');
16+
const os = require('os');
17+
const path = require('path');
18+
const Module = require('module');
19+
20+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'lc-reasoning-budget-'));
21+
const vscodeMock = { workspace: { workspaceFolders: [{ uri: { fsPath: root }, name: 'app' }] } };
22+
const origLoad = Module._load;
23+
// @ts-ignore
24+
Module._load = function (request, parent, isMain) {
25+
if (request === 'vscode') { return vscodeMock; }
26+
return origLoad.call(this, request, parent, isMain);
27+
};
28+
const providers = require('../providers/index');
29+
/** @type {any[]} */
30+
const asked = [];
31+
providers.streamAgentTurn = async (o) => {
32+
asked.push({ model: o.model, maxTokens: o.maxTokens, reasoningEffort: o.reasoningEffort });
33+
return { stop_reason: 'end_turn', content: [{ type: 'text', text: 'Done: nothing to change.' }] };
34+
};
35+
const { runAgent } = require('../agent');
36+
37+
let n = 0;
38+
async function test(name, fn) { await fn(); n++; console.log(' ok - ' + name); }
39+
40+
/** One single-turn run on `model` with the user's `maxTokens` setting; returns what that turn asked for. */
41+
async function turnFor(model, maxTokens) {
42+
asked.length = 0;
43+
await runAgent({
44+
providerId: 'openrouter', model, maxTokens, messages: [{ role: 'user', content: 'go' }], maxSteps: 2,
45+
post() {}, dbg() {}, approve: async () => true, commandRuns: new Map(), commandStops: new Map(), signal: new AbortController().signal,
46+
});
47+
assert.strictEqual(asked.length, 1, 'one turn');
48+
return asked[0];
49+
}
50+
51+
(async () => {
52+
try {
53+
await test('Gemini 4 Argon through OpenRouter or the gateway: 32,768 per turn and medium effort', async () => {
54+
assert.deepStrictEqual(await turnFor('google/gemini-4-argon', 8192), { model: 'google/gemini-4-argon', maxTokens: 32768, reasoningEffort: 'medium' });
55+
});
56+
await test('the bare Argon id also gets the ceiling, but asks for no effort', async () => {
57+
const t = await turnFor('gemini-4-argon', 8192);
58+
assert.strictEqual(t.maxTokens, 32768);
59+
assert.strictEqual(t.reasoningEffort, null);
60+
});
61+
await test('other reasoning models get the ceiling too (their reasoning shares the budget the same way)', async () => {
62+
assert.strictEqual((await turnFor('moonshotai/kimi-k3', 8192)).maxTokens, 32768);
63+
assert.strictEqual((await turnFor('o3-mini', 4096)).maxTokens, 32768);
64+
});
65+
await test('a model that does not reason keeps the user\'s setting, its clamp, and sends no effort', async () => {
66+
assert.deepStrictEqual(await turnFor('moonshotai/kimi-k2.7-code', 8192), { model: 'moonshotai/kimi-k2.7-code', maxTokens: 8192, reasoningEffort: null });
67+
assert.strictEqual((await turnFor('moonshotai/kimi-k2.7-code', 100000)).maxTokens, 32768, 'still clamped to 32,768');
68+
assert.strictEqual((await turnFor('moonshotai/kimi-k2.7-code', 10)).maxTokens, 1024, 'still at least 1,024');
69+
assert.strictEqual((await turnFor('anthropic/claude-opus-5', 0)).maxTokens, 8192, 'the default when nothing is set');
70+
});
71+
console.log('\nagentReasoningBudget: ' + n + ' tests passed.');
72+
} finally {
73+
Module._load = origLoad;
74+
fs.rmSync(root, { recursive: true, force: true });
75+
}
76+
})().catch((e) => { console.error(e); process.exit(1); });

‎extensions/levelcode-ai/test/catalog.test.js‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,35 @@ test('modelCaps: gpt-6-astra is an explicit row, not the gpt-4/5 heuristic', ()
2727
assert.strictEqual(C.supportsVisionForModel('openai', 'gpt-6-astra'), true);
2828
assert.strictEqual(C.contextWindowFor('openai', 'gpt-6-astra'), 1050000);
2929
});
30+
// Gemini 4 Argon reaches the picker from the gateway roster and from OpenRouter (BYOK). Its window is
31+
// 1,048,576, not the gemini heuristic's 1,000,000, and it reasons on every turn.
32+
test('modelCaps: gemini-4-argon is an explicit row under both ids; only the OpenRouter id asks for an effort', () => {
33+
const slug = C.modelCaps('google/gemini-4-argon');
34+
const bare = C.modelCaps('gemini-4-argon');
35+
for (const c of [slug, bare]) {
36+
assert.strictEqual(c.context, 1048576);
37+
assert.strictEqual(c.tools, true);
38+
assert.strictEqual(c.vision, true);
39+
assert.strictEqual(c.reasoning, true);
40+
assert.ok(!c.fast, 'never the ghost-text model');
41+
}
42+
assert.strictEqual(slug.effort, 'medium');
43+
assert.ok(!('effort' in bare), 'a bare id may reach an endpoint that was not checked for the field');
44+
assert.strictEqual(C.contextWindowFor('openrouter', 'google/gemini-4-argon'), 1048576);
45+
assert.strictEqual(C.supportsVisionForModel('openai', 'google/gemini-4-argon'), true); // the gateway's provider id
46+
assert.strictEqual(C.supportsToolsForModel('openrouter', 'google/gemini-4-argon'), true);
47+
assert.strictEqual(C.supportsToolsForModel('openai', 'google/gemini-4-argon'), true);
48+
assert.strictEqual(C.describeModel('google/gemini-4-argon'), '1049k ctx · reasoning · tools · vision');
49+
});
50+
test('reasoningEffortFor: medium for chat and the agent, low for compaction, none for a model without one', () => {
51+
assert.strictEqual(C.reasoningEffortFor('google/gemini-4-argon', 'chat'), 'medium');
52+
assert.strictEqual(C.reasoningEffortFor('google/gemini-4-argon', 'agent'), 'medium');
53+
assert.strictEqual(C.reasoningEffortFor('google/gemini-4-argon', 'compact'), 'low');
54+
for (const id of ['gemini-4-argon', 'moonshotai/kimi-k2.7-code', 'moonshotai/kimi-k3', 'o3-mini', 'anthropic/claude-opus-5', '']) {
55+
assert.strictEqual(C.reasoningEffortFor(id, 'agent'), null, id);
56+
assert.strictEqual(C.reasoningEffortFor(id, 'compact'), null, id);
57+
}
58+
});
3059
test('modelCaps: Fable 5 / 5.1 carry the 1M window under every id they arrive as', () => {
3160
for (const id of ['anthropic/claude-fable-5', 'anthropic/claude-fable-5.1', 'claude-fable-5-1']) {
3261
assert.deepStrictEqual(C.modelCaps(id), { context: 1000000, tools: true, vision: true, caching: true }, id);

0 commit comments

Comments
 (0)