add support for multi-agent Grok models (#197)
This commit is contained in:
1 parent
fc37434a2a
commit
994a307324
15 files changed
+578
-119
No files matched your search
+2
-2
@@ -4,8 +4,8 @@ GROK_API_KEY=your_grok_api_key_here
|
||||
# Optional: Custom API base URL (default: https://api.x.ai/v1)
|
||||
# GROK_BASE_URL=https://api.x.ai/v1
|
||||
|
||||
# Optional: Default model (default: grok-4-1-fast)
|
||||
# GROK_MODEL=grok-4-1-fast
|
||||
# Optional: Default model (default: grok-4-1-fast-reasoning)
|
||||
# GROK_MODEL=grok-4-1-fast-reasoning
|
||||
|
||||
# Optional: Max tokens per response (default: 16384)
|
||||
# GROK_MAX_TOKENS=16384
|
||||
|
||||
@@ -86,7 +86,7 @@ grok fix the flaky test in src/foo.test.ts
|
||||
|
||||
| Thing | What it means |
|
||||
|--------|----------------|
|
||||
| **Grok-native** | Defaults tuned for Grok; models like **`grok-code-fast-1`**, **`grok-4-1-fast`**, flagship and fast variants—run `grok models` for the full menu. |
|
||||
| **Grok-native** | Defaults tuned for Grok; models like **`grok-code-fast-1`**, **`grok-4-1-fast-reasoning`**, **`grok-4.20-multi-agent-0309`**, plus flagship and fast variants—run `grok models` for the full menu. |
|
||||
| **X + web search** | **`search_x`** and **`search_web`** tools—live posts and docs without pretending the internet stopped in 2023. |
|
||||
| **Sub-agents (default behavior)** | Foreground **`task`** delegation (e.g. explore vs general) plus background **`delegate`** for read-only deep dives—parallelize like you mean it. |
|
||||
| **Custom sub-agents** | Define named agents with **`subAgents`** in **`~/.grok/user-settings.json`** and manage them from the TUI with **`/agents`**. |
|
||||
|
||||
+61
-21
@@ -1,6 +1,6 @@
|
||||
import { type ModelMessage, stepCountIs, streamText, type ToolSet } from "ai";
|
||||
import { createProvider, generateTitle as genTitle, type XaiProvider } from "../grok/client";
|
||||
import { getModelInfo } from "../grok/models";
|
||||
import { createProvider, generateTitle as genTitle, resolveModelRuntime, type XaiProvider } from "../grok/client";
|
||||
import { DEFAULT_MODEL, getModelInfo, normalizeModelId } from "../grok/models";
|
||||
import { createTools } from "../grok/tools";
|
||||
import { buildMcpToolSet } from "../mcp/runtime";
|
||||
import {
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
shouldCompactContext,
|
||||
} from "./compaction";
|
||||
import { DelegationManager } from "./delegations";
|
||||
import { containsEncryptedReasoning, sanitizeModelMessages } from "./reasoning";
|
||||
|
||||
const MAX_TOOL_ROUNDS = 400;
|
||||
|
||||
@@ -287,6 +288,22 @@ function buildSubagentPrompt(
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function applyModelConstraints(system: string, modelId: string): string {
|
||||
const modelInfo = getModelInfo(modelId);
|
||||
if (modelInfo?.supportsClientTools !== false) {
|
||||
return system;
|
||||
}
|
||||
|
||||
return [
|
||||
system,
|
||||
"",
|
||||
"MODEL CONSTRAINTS:",
|
||||
"- The selected model does not support client-side CLI tool calls in this environment.",
|
||||
"- Do not call bash, read_file, write_file, edit_file, task, delegate, delegation, or MCP tools.",
|
||||
"- Answer directly using only the conversation context already provided.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export class Agent {
|
||||
private provider: XaiProvider | null = null;
|
||||
private apiKey: string | null = null;
|
||||
@@ -319,7 +336,7 @@ export class Agent {
|
||||
}
|
||||
this.bash = new BashTool();
|
||||
this.delegations = new DelegationManager(() => this.bash.getCwd());
|
||||
this.modelId = model || "grok-4-1-fast";
|
||||
this.modelId = normalizeModelId(model || DEFAULT_MODEL);
|
||||
this.maxToolRounds = maxToolRounds || MAX_TOOL_ROUNDS;
|
||||
const envMax = Number(process.env.GROK_MAX_TOKENS);
|
||||
this.maxTokens = Number.isFinite(envMax) && envMax > 0 ? envMax : 16_384;
|
||||
@@ -341,9 +358,9 @@ export class Agent {
|
||||
}
|
||||
|
||||
setModel(model: string): void {
|
||||
this.modelId = model;
|
||||
this.modelId = normalizeModelId(model);
|
||||
if (this.sessionStore && this.session) {
|
||||
this.sessionStore.setModel(this.session.id, model);
|
||||
this.sessionStore.setModel(this.session.id, this.modelId);
|
||||
this.session = this.sessionStore.getRequiredSession(this.session.id);
|
||||
}
|
||||
}
|
||||
@@ -410,7 +427,7 @@ export class Agent {
|
||||
}
|
||||
|
||||
const generated = await genTitle(provider, userMessage);
|
||||
this.recordUsage(generated.usage, "title", "grok-3-mini-fast");
|
||||
this.recordUsage(generated.usage, "title", generated.modelId);
|
||||
if (this.sessionStore && this.session && !this.session.title && generated.title) {
|
||||
this.sessionStore.setTitle(this.session.id, generated.title);
|
||||
this.session = this.sessionStore.getRequiredSession(this.session.id);
|
||||
@@ -553,12 +570,17 @@ export class Agent {
|
||||
let lastActivity = initialDetail;
|
||||
let childTools: ToolSet = childBaseTools;
|
||||
let closeMcp: (() => Promise<void>) | undefined;
|
||||
const childModelId = isExplore ? "grok-4-1-fast" : custom ? custom.model : this.modelId;
|
||||
const childModelId = normalizeModelId(isExplore ? DEFAULT_MODEL : custom ? custom.model : this.modelId);
|
||||
const childRuntime = resolveModelRuntime(provider, childModelId);
|
||||
const childSystem = applyModelConstraints(
|
||||
buildSubagentPrompt(request, childBash.getCwd(), custom ?? null, subagents),
|
||||
childRuntime.modelId,
|
||||
);
|
||||
|
||||
onActivity?.(initialDetail);
|
||||
|
||||
try {
|
||||
if (childMode === "agent") {
|
||||
if (childMode === "agent" && childRuntime.modelInfo?.supportsClientTools !== false) {
|
||||
const mcpBundle = await buildMcpToolSet(loadMcpServers());
|
||||
closeMcp = mcpBundle.close;
|
||||
childTools = { ...childBaseTools, ...mcpBundle.tools };
|
||||
@@ -569,17 +591,20 @@ export class Agent {
|
||||
}
|
||||
|
||||
const result = streamText({
|
||||
model: provider(childModelId),
|
||||
system: buildSubagentPrompt(request, childBash.getCwd(), custom ?? null, subagents),
|
||||
model: childRuntime.model,
|
||||
system: childSystem,
|
||||
messages: [{ role: "user", content: request.prompt }],
|
||||
tools: childTools,
|
||||
tools: childRuntime.modelInfo?.supportsClientTools === false ? {} : childTools,
|
||||
stopWhen: stepCountIs(Math.min(this.maxToolRounds, isExplore ? 60 : 120)),
|
||||
maxRetries: 0,
|
||||
abortSignal: signal,
|
||||
temperature: isExplore ? 0.2 : 0.5,
|
||||
maxOutputTokens: Math.min(this.maxTokens, 8_192),
|
||||
...(childRuntime.modelInfo?.supportsMaxOutputTokens === false
|
||||
? {}
|
||||
: { maxOutputTokens: Math.min(this.maxTokens, 8_192) }),
|
||||
...(childRuntime.providerOptions ? { providerOptions: childRuntime.providerOptions } : {}),
|
||||
onFinish: ({ totalUsage }) => {
|
||||
this.recordUsage(totalUsage, "task", childModelId);
|
||||
this.recordUsage(totalUsage, "task", childRuntime.modelId);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -789,14 +814,20 @@ export class Agent {
|
||||
|
||||
const provider = this.requireProvider();
|
||||
const subagents = loadValidSubAgents();
|
||||
const system = buildSystemPrompt(this.bash.getCwd(), this.mode, this.planContext, subagents);
|
||||
const modelInfo = getModelInfo(this.modelId);
|
||||
const system = applyModelConstraints(
|
||||
buildSystemPrompt(this.bash.getCwd(), this.mode, this.planContext, subagents),
|
||||
this.modelId,
|
||||
);
|
||||
const runtime = resolveModelRuntime(provider, this.modelId);
|
||||
const modelInfo = runtime.modelInfo;
|
||||
this.planContext = null;
|
||||
let attemptedOverflowRecovery = false;
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
let assistantText = "";
|
||||
let reasoningPreview = "";
|
||||
let encryptedReasoningHidden = false;
|
||||
let streamOk = false;
|
||||
let closeMcp: (() => Promise<void>) | undefined;
|
||||
let stepNumber = -1;
|
||||
@@ -824,8 +855,8 @@ export class Agent {
|
||||
listDelegations: () => this.listDelegations(),
|
||||
subagents,
|
||||
});
|
||||
let tools: ToolSet = baseTools;
|
||||
if (this.mode === "agent") {
|
||||
let tools: ToolSet = runtime.modelInfo?.supportsClientTools === false ? {} : baseTools;
|
||||
if (this.mode === "agent" && runtime.modelInfo?.supportsClientTools !== false) {
|
||||
const mcpBundle = await buildMcpToolSet(loadMcpServers());
|
||||
closeMcp = mcpBundle.close;
|
||||
tools = { ...baseTools, ...mcpBundle.tools };
|
||||
@@ -835,7 +866,7 @@ export class Agent {
|
||||
}
|
||||
|
||||
const result = streamText({
|
||||
model: provider(this.modelId),
|
||||
model: runtime.model,
|
||||
system,
|
||||
messages: this.messages,
|
||||
tools,
|
||||
@@ -843,7 +874,8 @@ export class Agent {
|
||||
maxRetries: 0,
|
||||
abortSignal: signal,
|
||||
temperature: 0.7,
|
||||
maxOutputTokens: this.maxTokens,
|
||||
...(runtime.modelInfo?.supportsMaxOutputTokens === false ? {} : { maxOutputTokens: this.maxTokens }),
|
||||
...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
|
||||
experimental_onStepStart: (event: unknown) => {
|
||||
stepNumber = getStepNumber(event, stepNumber + 1);
|
||||
notifyObserver(observer?.onStepStart, {
|
||||
@@ -862,7 +894,7 @@ export class Agent {
|
||||
});
|
||||
},
|
||||
onFinish: ({ totalUsage }) => {
|
||||
this.recordUsage(totalUsage, "message");
|
||||
this.recordUsage(totalUsage, "message", runtime.modelId);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -879,6 +911,14 @@ export class Agent {
|
||||
break;
|
||||
|
||||
case "reasoning-delta":
|
||||
reasoningPreview = `${reasoningPreview}${part.text}`.slice(-256);
|
||||
if (containsEncryptedReasoning(reasoningPreview)) {
|
||||
if (!encryptedReasoningHidden) {
|
||||
encryptedReasoningHidden = true;
|
||||
yield { type: "reasoning", content: "[Encrypted reasoning hidden]" };
|
||||
}
|
||||
break;
|
||||
}
|
||||
yield { type: "reasoning", content: part.text };
|
||||
break;
|
||||
|
||||
@@ -933,7 +973,7 @@ export class Agent {
|
||||
try {
|
||||
const response = await result.response;
|
||||
if (!signal.aborted) {
|
||||
this.appendCompletedTurn(userModelMessage, response.messages);
|
||||
this.appendCompletedTurn(userModelMessage, sanitizeModelMessages(response.messages));
|
||||
streamOk = true;
|
||||
}
|
||||
} catch (responseError: unknown) {
|
||||
|
||||
+14
-5
@@ -1,5 +1,6 @@
|
||||
import { generateText, type ModelMessage } from "ai";
|
||||
import type { XaiProvider } from "../grok/client";
|
||||
import { resolveModelRuntime, type XaiProvider } from "../grok/client";
|
||||
import { containsEncryptedReasoning } from "./reasoning";
|
||||
|
||||
export interface CompactionSettings {
|
||||
reserveTokens: number;
|
||||
@@ -110,11 +111,15 @@ function getTextParts(content: unknown): string[] {
|
||||
continue;
|
||||
}
|
||||
if (part.type === "reasoning" && typeof part.text === "string") {
|
||||
parts.push(part.text);
|
||||
if (!containsEncryptedReasoning(part.text)) {
|
||||
parts.push(part.text);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (part.type === "reasoning" && typeof part.reasoning === "string") {
|
||||
parts.push(part.reasoning);
|
||||
if (!containsEncryptedReasoning(part.reasoning)) {
|
||||
parts.push(part.reasoning);
|
||||
}
|
||||
}
|
||||
}
|
||||
return parts;
|
||||
@@ -409,14 +414,18 @@ async function summarizeConversation(
|
||||
promptParts.push(`Additional focus: ${customInstructions.trim()}`);
|
||||
}
|
||||
|
||||
const runtime = resolveModelRuntime(provider, modelId);
|
||||
const { text } = await generateText({
|
||||
model: provider(modelId),
|
||||
model: runtime.model,
|
||||
system: SUMMARIZATION_SYSTEM_PROMPT,
|
||||
prompt: promptParts.filter(Boolean).join("\n\n"),
|
||||
abortSignal: signal,
|
||||
maxRetries: 0,
|
||||
temperature: 0.2,
|
||||
maxOutputTokens: Math.max(512, Math.floor(reserveTokens * 0.8)),
|
||||
...(runtime.modelInfo?.supportsMaxOutputTokens === false
|
||||
? {}
|
||||
: { maxOutputTokens: Math.max(512, Math.floor(reserveTokens * 0.8)) }),
|
||||
...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
|
||||
});
|
||||
|
||||
return text.trim();
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { ModelMessage } from "ai";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { containsEncryptedReasoning, sanitizeModelMessages } from "./reasoning";
|
||||
|
||||
describe("reasoning helpers", () => {
|
||||
it("detects encrypted reasoning markers", () => {
|
||||
expect(containsEncryptedReasoning("-----BEGIN PGP MESSAGE-----\nabc")).toBe(true);
|
||||
expect(containsEncryptedReasoning("normal reasoning text")).toBe(false);
|
||||
});
|
||||
|
||||
it("removes encrypted reasoning from assistant messages", () => {
|
||||
const messages = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "reasoning", text: "-----BEGIN PGP MESSAGE-----\nabc" },
|
||||
{ type: "text", text: "Final answer" },
|
||||
],
|
||||
},
|
||||
] as ModelMessage[];
|
||||
|
||||
expect(sanitizeModelMessages(messages)).toEqual([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "Final answer" }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { ModelMessage } from "ai";
|
||||
|
||||
const ENCRYPTED_REASONING_MARKERS = [
|
||||
"-----BEGIN PGP MESSAGE-----",
|
||||
"-----BEGIN PGP ARMORED FILE-----",
|
||||
"-----BEGIN AGE ENCRYPTED FILE-----",
|
||||
"encrypted_content",
|
||||
] as const;
|
||||
type AssistantMessage = Extract<ModelMessage, { role: "assistant" }>;
|
||||
type AssistantContent = AssistantMessage["content"];
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function getReasoningText(part: unknown): string | null {
|
||||
if (!isRecord(part) || part.type !== "reasoning") return null;
|
||||
if (typeof part.text === "string") return part.text;
|
||||
if (typeof part.reasoning === "string") return part.reasoning;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function containsEncryptedReasoning(text: string): boolean {
|
||||
const lower = text.toLowerCase();
|
||||
return ENCRYPTED_REASONING_MARKERS.some((marker) => lower.includes(marker.toLowerCase()));
|
||||
}
|
||||
|
||||
function sanitizeAssistantContent(content: AssistantContent): AssistantContent {
|
||||
if (!Array.isArray(content)) return content;
|
||||
|
||||
const filtered = content.filter((part) => {
|
||||
const reasoningText = getReasoningText(part);
|
||||
return !reasoningText || !containsEncryptedReasoning(reasoningText);
|
||||
});
|
||||
|
||||
return filtered.length === content.length ? content : (filtered as typeof content);
|
||||
}
|
||||
|
||||
export function sanitizeModelMessages(messages: ModelMessage[]): ModelMessage[] {
|
||||
let changed = false;
|
||||
const sanitized: ModelMessage[] = [];
|
||||
|
||||
for (const message of messages) {
|
||||
if (message.role !== "assistant") {
|
||||
sanitized.push(message);
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = sanitizeAssistantContent(message.content);
|
||||
if (content !== message.content) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (Array.isArray(content) && content.length === 0) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextMessage: ModelMessage = content === message.content ? message : { ...message, content };
|
||||
sanitized.push(nextMessage);
|
||||
}
|
||||
|
||||
return changed ? sanitized : messages;
|
||||
}
|
||||
+46
-3
@@ -1,9 +1,19 @@
|
||||
import { createXai } from "@ai-sdk/xai";
|
||||
import { generateText } from "ai";
|
||||
import type { ModelInfo, ReasoningEffort } from "../types/index";
|
||||
import { getReasoningEffortForModel } from "../utils/settings";
|
||||
import { getEffectiveReasoningEffort, getModelInfo, normalizeModelId } from "./models";
|
||||
|
||||
export type XaiProvider = ReturnType<typeof createXai>;
|
||||
export type XaiChatModel = ReturnType<XaiProvider>;
|
||||
export type XaiResponsesModel = ReturnType<XaiProvider["responses"]>;
|
||||
export type GrokRuntimeModel = XaiChatModel | XaiResponsesModel;
|
||||
|
||||
const DEFAULT_TITLE_MODEL = "grok-4-1-fast-non-reasoning";
|
||||
|
||||
export interface GeneratedTitle {
|
||||
title: string;
|
||||
modelId: string;
|
||||
usage?: {
|
||||
totalTokens?: number;
|
||||
inputTokens?: number;
|
||||
@@ -11,6 +21,17 @@ export interface GeneratedTitle {
|
||||
};
|
||||
}
|
||||
|
||||
export interface ResolvedModelRuntime {
|
||||
model: GrokRuntimeModel;
|
||||
modelId: string;
|
||||
modelInfo?: ModelInfo;
|
||||
providerOptions?: {
|
||||
xai: {
|
||||
reasoningEffort: ReasoningEffort;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function createProvider(apiKey: string, baseURL?: string): XaiProvider {
|
||||
return createXai({
|
||||
apiKey,
|
||||
@@ -18,12 +39,33 @@ export function createProvider(apiKey: string, baseURL?: string): XaiProvider {
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveModelRuntime(provider: XaiProvider, requestedModelId: string): ResolvedModelRuntime {
|
||||
const modelId = normalizeModelId(requestedModelId);
|
||||
const modelInfo = getModelInfo(modelId);
|
||||
const reasoningEffort = getEffectiveReasoningEffort(modelId, getReasoningEffortForModel(modelId));
|
||||
|
||||
return {
|
||||
model: modelInfo?.responsesOnly ? provider.responses(modelId) : provider(modelId),
|
||||
modelId,
|
||||
modelInfo,
|
||||
providerOptions: reasoningEffort
|
||||
? {
|
||||
xai: {
|
||||
reasoningEffort,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateTitle(provider: XaiProvider, userMessage: string): Promise<GeneratedTitle> {
|
||||
const runtime = resolveModelRuntime(provider, DEFAULT_TITLE_MODEL);
|
||||
try {
|
||||
const { text, usage } = await generateText({
|
||||
model: provider("grok-3-mini-fast"),
|
||||
model: runtime.model,
|
||||
temperature: 0.5,
|
||||
maxOutputTokens: 60,
|
||||
...(runtime.modelInfo?.supportsMaxOutputTokens === false ? {} : { maxOutputTokens: 60 }),
|
||||
...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
|
||||
system: [
|
||||
"You are a title generator. Output ONLY a short title. Nothing else.",
|
||||
"Rules:",
|
||||
@@ -39,9 +81,10 @@ export async function generateTitle(provider: XaiProvider, userMessage: string):
|
||||
});
|
||||
return {
|
||||
title: text?.trim().replace(/^["']|["']$/g, "") || "New session",
|
||||
modelId: runtime.modelId,
|
||||
usage,
|
||||
};
|
||||
} catch {
|
||||
return { title: "New session" };
|
||||
return { title: "New session", modelId: runtime.modelId };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
DEFAULT_MODEL,
|
||||
getEffectiveReasoningEffort,
|
||||
getModelInfo,
|
||||
getSupportedReasoningEfforts,
|
||||
normalizeModelId,
|
||||
} from "./models";
|
||||
|
||||
describe("models", () => {
|
||||
it("keeps the default model on a canonical reasoning id", () => {
|
||||
expect(DEFAULT_MODEL).toBe("grok-4-1-fast-reasoning");
|
||||
});
|
||||
|
||||
it("normalizes current aliases to canonical ids", () => {
|
||||
expect(normalizeModelId("grok-4-1-fast")).toBe("grok-4-1-fast-reasoning");
|
||||
expect(normalizeModelId("grok-4.20-multi-agent")).toBe("grok-4.20-multi-agent-0309");
|
||||
expect(normalizeModelId("x-ai/grok-4.20-multi-agent-beta")).toBe("grok-4.20-multi-agent-0309");
|
||||
expect(normalizeModelId("xai/grok-code-fast-1")).toBe("grok-code-fast-1");
|
||||
});
|
||||
|
||||
it("returns model metadata for aliased ids", () => {
|
||||
expect(getModelInfo("grok-4-1-fast")?.id).toBe("grok-4-1-fast-reasoning");
|
||||
expect(getModelInfo("grok-4.20-multi-agent")?.responsesOnly).toBe(true);
|
||||
expect(getModelInfo("grok-4.20-multi-agent")?.supportsClientTools).toBe(false);
|
||||
});
|
||||
|
||||
it("reports supported reasoning-effort levels", () => {
|
||||
expect(getSupportedReasoningEfforts("grok-4.20-multi-agent-0309")).toEqual(["low", "medium", "high", "xhigh"]);
|
||||
expect(getSupportedReasoningEfforts("grok-4-1-fast-reasoning")).toEqual(["low", "high"]);
|
||||
expect(getSupportedReasoningEfforts("grok-4-1-fast-non-reasoning")).toEqual([]);
|
||||
});
|
||||
|
||||
it("resolves effective reasoning effort with defaults and overrides", () => {
|
||||
expect(getEffectiveReasoningEffort("grok-4.20-multi-agent-0309")).toBe("low");
|
||||
expect(getEffectiveReasoningEffort("grok-4.20-multi-agent-0309", "high")).toBe("high");
|
||||
expect(getEffectiveReasoningEffort("grok-4-1-fast-reasoning")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
+122
-35
@@ -1,6 +1,78 @@
|
||||
import type { ModelInfo } from "../types/index";
|
||||
import type { ModelInfo, ReasoningEffort } from "../types/index";
|
||||
|
||||
export const MODELS: ModelInfo[] = [
|
||||
{
|
||||
id: "grok-4.20-multi-agent-0309",
|
||||
name: "Grok 4.20 Multi-Agent",
|
||||
contextWindow: 2_000_000,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 6.0,
|
||||
reasoning: true,
|
||||
description: "Realtime multi-agent research model",
|
||||
aliases: ["grok-4.20-multi-agent", "grok-4.20-multi-agent-beta"],
|
||||
responsesOnly: true,
|
||||
multiAgent: true,
|
||||
supportsClientTools: false,
|
||||
supportsMaxOutputTokens: false,
|
||||
defaultReasoningEffort: "low",
|
||||
},
|
||||
{
|
||||
id: "grok-4.20-0309-reasoning",
|
||||
name: "Grok 4.20 Reasoning",
|
||||
contextWindow: 2_000_000,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 6.0,
|
||||
reasoning: true,
|
||||
description: "Grok 4.20 reasoning release",
|
||||
aliases: ["grok-4.20-beta-0309", "grok-4.20-beta", "grok-beta"],
|
||||
},
|
||||
{
|
||||
id: "grok-4.20-0309-non-reasoning",
|
||||
name: "Grok 4.20 Non-Reasoning",
|
||||
contextWindow: 2_000_000,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 6.0,
|
||||
reasoning: false,
|
||||
description: "Grok 4.20 non-reasoning release",
|
||||
},
|
||||
{
|
||||
id: "grok-4-1-fast-reasoning",
|
||||
name: "Grok 4.1 Fast Reasoning",
|
||||
contextWindow: 2_000_000,
|
||||
inputPrice: 0.2,
|
||||
outputPrice: 0.5,
|
||||
reasoning: true,
|
||||
description: "Fast reasoning model with 2M context",
|
||||
aliases: ["grok-4-1-fast"],
|
||||
},
|
||||
{
|
||||
id: "grok-4-1-fast-non-reasoning",
|
||||
name: "Grok 4.1 Fast Non-Reasoning",
|
||||
contextWindow: 2_000_000,
|
||||
inputPrice: 0.2,
|
||||
outputPrice: 0.5,
|
||||
reasoning: false,
|
||||
description: "Fast non-reasoning model with 2M context",
|
||||
},
|
||||
{
|
||||
id: "grok-4-fast-reasoning",
|
||||
name: "Grok 4 Fast Reasoning",
|
||||
contextWindow: 2_000_000,
|
||||
inputPrice: 0.2,
|
||||
outputPrice: 0.5,
|
||||
reasoning: true,
|
||||
description: "Legacy fast reasoning model",
|
||||
aliases: ["grok-4-fast"],
|
||||
},
|
||||
{
|
||||
id: "grok-4-fast-non-reasoning",
|
||||
name: "Grok 4 Fast Non-Reasoning",
|
||||
contextWindow: 2_000_000,
|
||||
inputPrice: 0.2,
|
||||
outputPrice: 0.5,
|
||||
reasoning: false,
|
||||
description: "Legacy fast non-reasoning model",
|
||||
},
|
||||
{
|
||||
id: "grok-4-0709",
|
||||
name: "Grok 4",
|
||||
@@ -10,46 +82,20 @@ export const MODELS: ModelInfo[] = [
|
||||
reasoning: true,
|
||||
description: "Flagship reasoning model",
|
||||
},
|
||||
{
|
||||
id: "grok-4.20-beta-0309",
|
||||
name: "Grok 4.20 Beta",
|
||||
contextWindow: 2_000_000,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 6.0,
|
||||
reasoning: true,
|
||||
description: "Multi-agent reasoning beta with 2M context",
|
||||
},
|
||||
{
|
||||
id: "grok-4-fast",
|
||||
name: "Grok 4 Fast",
|
||||
contextWindow: 2_000_000,
|
||||
inputPrice: 0.2,
|
||||
outputPrice: 0.5,
|
||||
reasoning: true,
|
||||
description: "Fast reasoning with 2M context",
|
||||
},
|
||||
{
|
||||
id: "grok-4-1-fast",
|
||||
name: "Grok 4.1 Fast",
|
||||
contextWindow: 2_000_000,
|
||||
inputPrice: 0.2,
|
||||
outputPrice: 0.5,
|
||||
reasoning: true,
|
||||
description: "Latest fast model with 2M context",
|
||||
},
|
||||
{
|
||||
id: "grok-code-fast-1",
|
||||
name: "Grok Code Fast",
|
||||
name: "Grok Code Fast 1",
|
||||
contextWindow: 256_000,
|
||||
inputPrice: 0.2,
|
||||
outputPrice: 1.5,
|
||||
reasoning: false,
|
||||
description: "Optimized for code generation",
|
||||
reasoning: true,
|
||||
description: "Fast coding model with reasoning",
|
||||
aliases: ["grok-code-fast"],
|
||||
},
|
||||
{
|
||||
id: "grok-3",
|
||||
name: "Grok 3",
|
||||
contextWindow: 131_000,
|
||||
contextWindow: 131_072,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
reasoning: false,
|
||||
@@ -58,20 +104,61 @@ export const MODELS: ModelInfo[] = [
|
||||
{
|
||||
id: "grok-3-mini",
|
||||
name: "Grok 3 Mini",
|
||||
contextWindow: 131_000,
|
||||
contextWindow: 131_072,
|
||||
inputPrice: 0.3,
|
||||
outputPrice: 0.5,
|
||||
reasoning: false,
|
||||
description: "Budget-friendly compact model",
|
||||
aliases: ["grok-3-mini-fast"],
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_MODEL = "grok-4-1-fast";
|
||||
const PROVIDER_PREFIX_RE = /^(x-ai|xai)\//i;
|
||||
const aliasMap = new Map<string, string>();
|
||||
|
||||
for (const model of MODELS) {
|
||||
aliasMap.set(model.id.toLowerCase(), model.id);
|
||||
for (const alias of model.aliases ?? []) {
|
||||
aliasMap.set(alias.toLowerCase(), model.id);
|
||||
}
|
||||
}
|
||||
|
||||
export const DEFAULT_MODEL =
|
||||
MODELS.find((model) => model.id === "grok-4-1-fast-reasoning")?.id ?? MODELS[0]?.id ?? "grok-4-1-fast-reasoning";
|
||||
|
||||
export function normalizeModelId(modelId: string): string {
|
||||
const trimmed = modelId.trim();
|
||||
if (!trimmed) return trimmed;
|
||||
|
||||
const withoutProviderPrefix = trimmed.replace(PROVIDER_PREFIX_RE, "");
|
||||
return aliasMap.get(withoutProviderPrefix.toLowerCase()) ?? withoutProviderPrefix;
|
||||
}
|
||||
|
||||
export function getModelInfo(modelId: string): ModelInfo | undefined {
|
||||
return MODELS.find((m) => m.id === modelId);
|
||||
const normalized = normalizeModelId(modelId);
|
||||
return MODELS.find((m) => m.id === normalized);
|
||||
}
|
||||
|
||||
export function getModelIds(): string[] {
|
||||
return MODELS.map((m) => m.id);
|
||||
}
|
||||
|
||||
export function isKnownModelId(modelId: string): boolean {
|
||||
return !!getModelInfo(modelId);
|
||||
}
|
||||
|
||||
export function getSupportedReasoningEfforts(modelId: string): ReasoningEffort[] {
|
||||
const modelInfo = getModelInfo(modelId);
|
||||
if (!modelInfo?.reasoning) return [];
|
||||
if (modelInfo.multiAgent) return ["low", "medium", "high", "xhigh"];
|
||||
if (modelInfo.responsesOnly) return ["low", "medium", "high"];
|
||||
return ["low", "high"];
|
||||
}
|
||||
|
||||
export function getEffectiveReasoningEffort(modelId: string, override?: ReasoningEffort): ReasoningEffort | undefined {
|
||||
const supported = getSupportedReasoningEfforts(modelId);
|
||||
if (supported.length === 0) return undefined;
|
||||
if (override && supported.includes(override)) return override;
|
||||
const defaultEffort = getModelInfo(modelId)?.defaultReasoningEffort;
|
||||
return defaultEffort && supported.includes(defaultEffort) ? defaultEffort : undefined;
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@ import type { AgentMode, TaskRequest, ToolResult } from "../types/index";
|
||||
import { type CustomSubagentConfig, loadValidSubAgents } from "../utils/settings";
|
||||
import type { XaiProvider } from "./client";
|
||||
|
||||
const RESPONSES_SEARCH_MODEL = "grok-4-fast-non-reasoning";
|
||||
const RESPONSES_SEARCH_MODEL = "grok-4-1-fast-non-reasoning";
|
||||
|
||||
interface CreateToolsOptions {
|
||||
runTask?: (request: TaskRequest, abortSignal?: AbortSignal) => Promise<ToolResult>;
|
||||
|
||||
+14
-6
@@ -4,7 +4,7 @@ import * as dotenv from "dotenv";
|
||||
import packageJson from "../package.json";
|
||||
import { Agent } from "./agent/agent";
|
||||
import { completeDelegation, failDelegation, loadDelegation } from "./agent/delegations";
|
||||
import { MODELS } from "./grok/models";
|
||||
import { MODELS, normalizeModelId } from "./grok/models";
|
||||
import {
|
||||
createHeadlessJsonlEmitter,
|
||||
type HeadlessOutputFormat,
|
||||
@@ -118,7 +118,7 @@ async function runBackgroundDelegation(jobPath: string, options: Record<string,
|
||||
}
|
||||
|
||||
const baseURL = options.baseUrl || getBaseURL();
|
||||
const model = options.model || delegation.model || getCurrentModel();
|
||||
const model = normalizeModelId(options.model || delegation.model || getCurrentModel());
|
||||
const maxToolRounds =
|
||||
parseInt(options.maxToolRounds || String(delegation.maxToolRounds), 10) || delegation.maxToolRounds;
|
||||
const agent = new Agent(apiKey, baseURL, model, maxToolRounds, { persistSession: false });
|
||||
@@ -150,11 +150,11 @@ async function runBackgroundDelegation(jobPath: string, options: Record<string,
|
||||
function resolveConfig(options: Record<string, string | undefined>) {
|
||||
const apiKey = options.apiKey || getApiKey();
|
||||
const baseURL = options.baseUrl || getBaseURL();
|
||||
const model = options.model || getCurrentModel();
|
||||
const model = normalizeModelId(options.model || getCurrentModel());
|
||||
const maxToolRounds = parseInt(options.maxToolRounds || "400", 10) || 400;
|
||||
|
||||
if (options.apiKey) saveUserSettings({ apiKey: options.apiKey });
|
||||
if (options.model) saveUserSettings({ defaultModel: options.model });
|
||||
if (options.model) saveUserSettings({ defaultModel: normalizeModelId(options.model) });
|
||||
|
||||
return { apiKey, baseURL, model, maxToolRounds };
|
||||
}
|
||||
@@ -240,11 +240,19 @@ program
|
||||
.action(() => {
|
||||
console.log("\nAvailable Grok Models:\n");
|
||||
for (const m of MODELS) {
|
||||
const reasoning = m.reasoning ? " (reasoning)" : "";
|
||||
console.log(` \x1b[36m${m.id}\x1b[0m — ${m.name}${reasoning}`);
|
||||
const tags = [
|
||||
m.reasoning ? "reasoning" : "non-reasoning",
|
||||
m.multiAgent ? "multi-agent" : null,
|
||||
m.responsesOnly ? "responses-only" : null,
|
||||
].filter(Boolean);
|
||||
const suffix = tags.length > 0 ? ` (${tags.join(", ")})` : "";
|
||||
console.log(` \x1b[36m${m.id}\x1b[0m — ${m.name}${suffix}`);
|
||||
console.log(
|
||||
` ${m.description} | ${formatContext(m.contextWindow)} context | $${m.inputPrice}/$${m.outputPrice} per 1M tokens`,
|
||||
);
|
||||
if ((m.aliases?.length ?? 0) > 0) {
|
||||
console.log(` aliases: ${(m.aliases ?? []).join(", ")}`);
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
});
|
||||
|
||||
@@ -107,6 +107,8 @@ export interface StreamChunk {
|
||||
toolResult?: ToolResult;
|
||||
}
|
||||
|
||||
export type ReasoningEffort = "low" | "medium" | "high" | "xhigh";
|
||||
|
||||
export interface ModelInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -115,6 +117,12 @@ export interface ModelInfo {
|
||||
outputPrice: number;
|
||||
reasoning: boolean;
|
||||
description: string;
|
||||
aliases?: string[];
|
||||
responsesOnly?: boolean;
|
||||
multiAgent?: boolean;
|
||||
supportsClientTools?: boolean;
|
||||
supportsMaxOutputTokens?: boolean;
|
||||
defaultReasoningEffort?: ReasoningEffort;
|
||||
}
|
||||
|
||||
export type AgentMode = "agent" | "plan" | "ask";
|
||||
|
||||
+120
-30
@@ -4,7 +4,15 @@ import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
|
||||
import os from "os";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Agent } from "../agent/agent";
|
||||
import { getModelIds, getModelInfo, MODELS } from "../grok/models";
|
||||
import {
|
||||
DEFAULT_MODEL,
|
||||
getEffectiveReasoningEffort,
|
||||
getModelIds,
|
||||
getModelInfo,
|
||||
getSupportedReasoningEfforts,
|
||||
MODELS,
|
||||
normalizeModelId,
|
||||
} from "../grok/models";
|
||||
import { POPULAR_MCP_CATALOG } from "../mcp/catalog";
|
||||
import { parseEnvLines, parseHeaderLines } from "../mcp/parse-headers";
|
||||
import { toMcpServerId, validateMcpServerConfig } from "../mcp/validate";
|
||||
@@ -18,6 +26,7 @@ import type {
|
||||
ModelInfo,
|
||||
Plan,
|
||||
PlanQuestion,
|
||||
ReasoningEffort,
|
||||
SubagentStatus,
|
||||
ToolCall,
|
||||
ToolResult,
|
||||
@@ -77,6 +86,14 @@ type ContextStats = {
|
||||
ratioUsed: number;
|
||||
ratioRemaining: number;
|
||||
};
|
||||
type PasteBlock = { id: number; content: string; lines: number; isImage?: boolean };
|
||||
|
||||
function getPasteBlockToken(block: Pick<PasteBlock, "id" | "lines" | "isImage">): string {
|
||||
if (block.isImage) {
|
||||
return `[Image #${block.id}]`;
|
||||
}
|
||||
return `[Pasted #${block.id} ${block.lines}+ lines]`;
|
||||
}
|
||||
|
||||
const HERO_ROWS: Row[] = [
|
||||
{
|
||||
@@ -325,16 +342,22 @@ export function App({ agent, startupConfig, initialMessage, onExit }: AppProps)
|
||||
const [showSlashMenu, setShowSlashMenu] = useState(false);
|
||||
const [slashMenuIndex, setSlashMenuIndex] = useState(0);
|
||||
const [slashSearchQuery, setSlashSearchQuery] = useState("");
|
||||
const [pasteBlocks, setPasteBlocks] = useState<{ id: number; content: string; lines: number; isImage?: boolean }[]>(
|
||||
[],
|
||||
const [reasoningEffortByModel, setReasoningEffortByModel] = useState<Record<string, ReasoningEffort>>(() =>
|
||||
Object.fromEntries(
|
||||
Object.entries(loadUserSettings().reasoningEffortByModel ?? {}).map(([modelId, effort]) => [
|
||||
normalizeModelId(modelId),
|
||||
effort,
|
||||
]),
|
||||
),
|
||||
);
|
||||
const [pasteBlocks, setPasteBlocks] = useState<PasteBlock[]>([]);
|
||||
const [activePlan, setActivePlan] = useState<Plan | null>(null);
|
||||
/** Incremented on each successful TUI copy; drives a brief "Copied" banner. */
|
||||
const [copyFlashId, setCopyFlashId] = useState(0);
|
||||
const [activeSubagent, setActiveSubagent] = useState<SubagentStatus | null>(null);
|
||||
const [pqs, setPqs] = useState<PlanQuestionsState>(initialPlanQuestionsState());
|
||||
const imageCounterRef = useRef(0);
|
||||
const pasteCounterRef = useRef(0);
|
||||
const pasteBlocksRef = useRef<PasteBlock[]>([]);
|
||||
const apiKeyInputRef = useRef<TextareaRenderable>(null);
|
||||
const inputRef = useRef<TextareaRenderable>(null);
|
||||
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
||||
@@ -394,7 +417,7 @@ export function App({ agent, startupConfig, initialMessage, onExit }: AppProps)
|
||||
const [agentsEditorModelIndex, setAgentsEditorModelIndex] = useState(() =>
|
||||
Math.max(
|
||||
0,
|
||||
MODELS.findIndex((model) => model.id === "grok-4-1-fast"),
|
||||
MODELS.findIndex((model) => model.id === DEFAULT_MODEL),
|
||||
),
|
||||
);
|
||||
const [agentsEditorSyncKey, setAgentsEditorSyncKey] = useState(0);
|
||||
@@ -460,6 +483,51 @@ export function App({ agent, startupConfig, initialMessage, onExit }: AppProps)
|
||||
saveMcpServers(servers);
|
||||
}, []);
|
||||
|
||||
const setReasoningEfforts = useCallback((next: Record<string, ReasoningEffort>) => {
|
||||
setReasoningEffortByModel(next);
|
||||
saveUserSettings({ reasoningEffortByModel: next });
|
||||
}, []);
|
||||
|
||||
const replacePasteBlocks = useCallback((next: PasteBlock[]) => {
|
||||
pasteBlocksRef.current = next;
|
||||
setPasteBlocks(next);
|
||||
}, []);
|
||||
|
||||
const getModelReasoningEffort = useCallback(
|
||||
(modelId: string): ReasoningEffort | undefined => {
|
||||
const normalizedModelId = normalizeModelId(modelId);
|
||||
return getEffectiveReasoningEffort(normalizedModelId, reasoningEffortByModel[normalizedModelId]);
|
||||
},
|
||||
[reasoningEffortByModel],
|
||||
);
|
||||
|
||||
const adjustModelReasoningEffort = useCallback(
|
||||
(modelId: string, direction: -1 | 1) => {
|
||||
const normalizedModelId = normalizeModelId(modelId);
|
||||
const supported = getSupportedReasoningEfforts(normalizedModelId);
|
||||
if (supported.length === 0) return;
|
||||
|
||||
const current = getModelReasoningEffort(normalizedModelId);
|
||||
|
||||
if (!current) {
|
||||
if (direction > 0) {
|
||||
setReasoningEfforts({ ...reasoningEffortByModel, [normalizedModelId]: supported[0] });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const currentIndex = supported.indexOf(current);
|
||||
if (direction < 0 && currentIndex <= 0) {
|
||||
const { [normalizedModelId]: _, ...rest } = reasoningEffortByModel;
|
||||
setReasoningEfforts(rest);
|
||||
} else {
|
||||
const nextIndex = direction < 0 ? currentIndex - 1 : Math.min(supported.length - 1, currentIndex + 1);
|
||||
setReasoningEfforts({ ...reasoningEffortByModel, [normalizedModelId]: supported[nextIndex] });
|
||||
}
|
||||
},
|
||||
[getModelReasoningEffort, reasoningEffortByModel, setReasoningEfforts],
|
||||
);
|
||||
|
||||
const snapshotMcpEditorDraft = useCallback((): McpEditorDraft => {
|
||||
return {
|
||||
...mcpEditorDraft,
|
||||
@@ -583,7 +651,7 @@ export function App({ agent, startupConfig, initialMessage, onExit }: AppProps)
|
||||
setAgentsEditorModelIndex(
|
||||
Math.max(
|
||||
0,
|
||||
MODELS.findIndex((model) => model.id === agent.model),
|
||||
MODELS.findIndex((model) => model.id === normalizeModelId(agent.model)),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
@@ -591,7 +659,7 @@ export function App({ agent, startupConfig, initialMessage, onExit }: AppProps)
|
||||
setAgentsEditorModelIndex(
|
||||
Math.max(
|
||||
0,
|
||||
MODELS.findIndex((model) => model.id === "grok-4-1-fast"),
|
||||
MODELS.findIndex((model) => model.id === DEFAULT_MODEL),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1245,11 +1313,10 @@ export function App({ agent, startupConfig, initialMessage, onExit }: AppProps)
|
||||
setActiveSubagent(null);
|
||||
setActivePlan(null);
|
||||
setPqs(initialPlanQuestionsState());
|
||||
setPasteBlocks([]);
|
||||
replacePasteBlocks([]);
|
||||
queuedMessagesRef.current = [];
|
||||
setQueuedMessages([]);
|
||||
imageCounterRef.current = 0;
|
||||
}, [agent]);
|
||||
}, [agent, replacePasteBlocks]);
|
||||
|
||||
const processMessage = useCallback(
|
||||
async (text: string) => {
|
||||
@@ -1972,6 +2039,13 @@ export function App({ agent, startupConfig, initialMessage, onExit }: AppProps)
|
||||
setModelPickerIndex((i) => Math.min(filteredModelIds.length - 1, i + 1));
|
||||
return;
|
||||
}
|
||||
if (key.name === "left" || key.name === "right") {
|
||||
const sel = filteredModelIds[modelPickerIndex];
|
||||
if (sel) {
|
||||
adjustModelReasoningEffort(sel, key.name === "left" ? -1 : 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.name === "return") {
|
||||
const sel = filteredModelIds[modelPickerIndex];
|
||||
if (sel) {
|
||||
@@ -2044,7 +2118,7 @@ export function App({ agent, startupConfig, initialMessage, onExit }: AppProps)
|
||||
const text = inputRef.current?.plainText || "";
|
||||
if (text.trim()) {
|
||||
inputRef.current?.clear();
|
||||
setPasteBlocks([]);
|
||||
replacePasteBlocks([]);
|
||||
} else {
|
||||
handleExit();
|
||||
}
|
||||
@@ -2069,6 +2143,7 @@ export function App({ agent, startupConfig, initialMessage, onExit }: AppProps)
|
||||
dismissPlan,
|
||||
editingSubagent,
|
||||
editSavedMcp,
|
||||
adjustModelReasoningEffort,
|
||||
filteredModelIds,
|
||||
filteredSlashItems,
|
||||
handleExit,
|
||||
@@ -2086,6 +2161,7 @@ export function App({ agent, startupConfig, initialMessage, onExit }: AppProps)
|
||||
openApiKeyModal,
|
||||
openCatalogMcp,
|
||||
openMcpEditor,
|
||||
replacePasteBlocks,
|
||||
openSubagentEditor,
|
||||
submitTelegramPair,
|
||||
submitTelegramToken,
|
||||
@@ -2121,24 +2197,25 @@ export function App({ agent, startupConfig, initialMessage, onExit }: AppProps)
|
||||
if (imageExts.test(trimmed) && !trimmed.includes("\n")) {
|
||||
event.preventDefault();
|
||||
const id = ++pasteCounterRef.current;
|
||||
const imgNum = ++imageCounterRef.current;
|
||||
setPasteBlocks((prev) => [...prev, { id, content: trimmed, lines: 1, isImage: true }]);
|
||||
inputRef.current?.insertText(`[Image ${imgNum}]`);
|
||||
const block = { id, content: trimmed, lines: 1, isImage: true } satisfies PasteBlock;
|
||||
replacePasteBlocks([...pasteBlocksRef.current, block]);
|
||||
inputRef.current?.insertText(getPasteBlockToken(block));
|
||||
return;
|
||||
}
|
||||
const lineCount = text.split("\n").length;
|
||||
if (lineCount < 2) return;
|
||||
event.preventDefault();
|
||||
const id = ++pasteCounterRef.current;
|
||||
setPasteBlocks((prev) => [...prev, { id, content: text, lines: lineCount }]);
|
||||
inputRef.current?.insertText(`[Pasted ~${lineCount} lines]`);
|
||||
const block = { id, content: text, lines: lineCount } satisfies PasteBlock;
|
||||
replacePasteBlocks([...pasteBlocksRef.current, block]);
|
||||
inputRef.current?.insertText(getPasteBlockToken(block));
|
||||
},
|
||||
[openApiKeyModal],
|
||||
[openApiKeyModal, replacePasteBlocks],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
const raw = inputRef.current?.plainText || "";
|
||||
if (!raw.trim() && pasteBlocks.length === 0) {
|
||||
if (!raw.trim() && pasteBlocksRef.current.length === 0) {
|
||||
if (queuedMessagesRef.current.length > 0 && isProcessingRef.current) {
|
||||
interruptedRunIdRef.current = activeRunIdRef.current;
|
||||
setStreamContent("");
|
||||
@@ -2151,16 +2228,10 @@ export function App({ agent, startupConfig, initialMessage, onExit }: AppProps)
|
||||
}
|
||||
inputRef.current?.clear();
|
||||
let message = raw;
|
||||
const blocks = [...pasteBlocks];
|
||||
let imgIdx = 0;
|
||||
setPasteBlocks([]);
|
||||
const blocks = [...pasteBlocksRef.current];
|
||||
replacePasteBlocks([]);
|
||||
for (const block of blocks) {
|
||||
if (block.isImage) {
|
||||
imgIdx++;
|
||||
message = message.replace(`[Image ${imgIdx}]`, block.content);
|
||||
} else {
|
||||
message = message.replace(`[Pasted ~${block.lines} lines]`, block.content);
|
||||
}
|
||||
message = message.replace(getPasteBlockToken(block), block.content);
|
||||
}
|
||||
if (!message.trim()) return;
|
||||
if (!hasApiKeyRef.current) {
|
||||
@@ -2175,7 +2246,7 @@ export function App({ agent, startupConfig, initialMessage, onExit }: AppProps)
|
||||
return;
|
||||
}
|
||||
processMessage(message);
|
||||
}, [agent, handleCommand, openApiKeyModal, processMessage, pasteBlocks, scrollToBottom]);
|
||||
}, [agent, handleCommand, openApiKeyModal, processMessage, replacePasteBlocks, scrollToBottom]);
|
||||
|
||||
const hasMessages = messages.length > 0 || streamContent || isProcessing;
|
||||
|
||||
@@ -2387,6 +2458,7 @@ export function App({ agent, startupConfig, initialMessage, onExit }: AppProps)
|
||||
height={height}
|
||||
searchQuery={modelSearchQuery}
|
||||
filteredModels={filteredModels}
|
||||
reasoningEffortByModel={reasoningEffortByModel}
|
||||
/>
|
||||
)}
|
||||
{showConnectModal && (
|
||||
@@ -3652,6 +3724,7 @@ function ModelPickerModal({
|
||||
height,
|
||||
searchQuery,
|
||||
filteredModels,
|
||||
reasoningEffortByModel,
|
||||
}: {
|
||||
t: Theme;
|
||||
currentModel: string;
|
||||
@@ -3660,6 +3733,7 @@ function ModelPickerModal({
|
||||
height: number;
|
||||
searchQuery: string;
|
||||
filteredModels: ModelInfo[];
|
||||
reasoningEffortByModel: Record<string, ReasoningEffort>;
|
||||
}) {
|
||||
const listRef = useRef<ScrollBoxRenderable>(null);
|
||||
useEffect(() => {
|
||||
@@ -3668,7 +3742,9 @@ function ModelPickerModal({
|
||||
}, [selectedIndex, filteredModels]);
|
||||
|
||||
const itemCount = Math.max(filteredModels.length, 1);
|
||||
const contentHeight = itemCount + 5;
|
||||
const selectedModel = filteredModels[selectedIndex];
|
||||
const selectedSupportsReasoning = !!selectedModel && getSupportedReasoningEfforts(selectedModel.id).length > 0;
|
||||
const contentHeight = itemCount + 6;
|
||||
const maxH = Math.floor(height * 0.6);
|
||||
const panelHeight = Math.min(contentHeight, maxH);
|
||||
const top = bottomAlignedModalTop(height, panelHeight);
|
||||
@@ -3705,6 +3781,9 @@ function ModelPickerModal({
|
||||
{filteredModels.map((m, idx) => {
|
||||
const selected = idx === selectedIndex;
|
||||
const current = m.id === currentModel;
|
||||
const supportedReasoningEfforts = getSupportedReasoningEfforts(m.id);
|
||||
const reasoningEffort =
|
||||
getEffectiveReasoningEffort(m.id, reasoningEffortByModel[normalizeModelId(m.id)]) ?? "auto";
|
||||
return (
|
||||
<box
|
||||
key={m.id}
|
||||
@@ -3712,8 +3791,14 @@ function ModelPickerModal({
|
||||
backgroundColor={selected ? t.selectedBg : undefined}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
width="100%"
|
||||
>
|
||||
<text fg={current ? t.accent : selected ? t.selected : t.text}>{m.name}</text>
|
||||
<box width="100%" flexDirection="row" justifyContent="space-between">
|
||||
<text fg={current ? t.accent : selected ? t.selected : t.text}>{m.name}</text>
|
||||
{supportedReasoningEfforts.length > 0 ? (
|
||||
<text fg={selected ? t.primary : t.textMuted}>{`[${reasoningEffort}]`}</text>
|
||||
) : null}
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
})}
|
||||
@@ -3723,6 +3808,11 @@ function ModelPickerModal({
|
||||
</box>
|
||||
)}
|
||||
</scrollbox>
|
||||
<box flexShrink={0} paddingLeft={2} paddingRight={2} paddingTop={1}>
|
||||
<text fg={t.textMuted}>
|
||||
{selectedSupportsReasoning ? "left/right reasoning enter select esc close" : "enter select esc close"}
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
|
||||
+40
-8
@@ -1,7 +1,8 @@
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import { DEFAULT_MODEL, getModelIds } from "../grok/models";
|
||||
import { DEFAULT_MODEL, getEffectiveReasoningEffort, getModelIds, normalizeModelId } from "../grok/models";
|
||||
import type { ReasoningEffort } from "../types/index";
|
||||
|
||||
export type TelegramStreamingMode = "off" | "partial";
|
||||
|
||||
@@ -60,7 +61,7 @@ export function parseSubAgentsRawList(raw: unknown): CustomSubagentConfig[] {
|
||||
|
||||
const entry = item as Record<string, unknown>;
|
||||
const name = typeof entry.name === "string" ? entry.name.trim() : "";
|
||||
const model = typeof entry.model === "string" ? entry.model.trim() : "";
|
||||
const model = typeof entry.model === "string" ? normalizeModelId(entry.model) : "";
|
||||
const instruction = typeof entry.instruction === "string" ? entry.instruction : "";
|
||||
|
||||
if (!name || isReservedSubagentName(name) || !validModels.has(model)) {
|
||||
@@ -84,6 +85,7 @@ export function loadValidSubAgents(): CustomSubagentConfig[] {
|
||||
export interface UserSettings {
|
||||
apiKey?: string;
|
||||
defaultModel?: string;
|
||||
reasoningEffortByModel?: Record<string, ReasoningEffort>;
|
||||
telegram?: TelegramSettings;
|
||||
mcp?: McpSettings;
|
||||
subAgents?: CustomSubagentConfig[];
|
||||
@@ -126,7 +128,17 @@ export function saveUserSettings(partial: Partial<UserSettings>): void {
|
||||
...current,
|
||||
...partial,
|
||||
...(partial.apiKey !== undefined ? { apiKey: partial.apiKey } : {}),
|
||||
...(partial.defaultModel !== undefined ? { defaultModel: partial.defaultModel } : {}),
|
||||
...(partial.defaultModel !== undefined ? { defaultModel: normalizeModelId(partial.defaultModel) } : {}),
|
||||
...(partial.reasoningEffortByModel !== undefined
|
||||
? {
|
||||
reasoningEffortByModel: Object.fromEntries(
|
||||
Object.entries(partial.reasoningEffortByModel).map(([modelId, effort]) => [
|
||||
normalizeModelId(modelId),
|
||||
effort,
|
||||
]),
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
...(partial.telegram !== undefined
|
||||
? {
|
||||
telegram: {
|
||||
@@ -148,7 +160,14 @@ export function saveUserSettings(partial: Partial<UserSettings>): void {
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(partial.subAgents !== undefined ? { subAgents: partial.subAgents } : {}),
|
||||
...(partial.subAgents !== undefined
|
||||
? {
|
||||
subAgents: partial.subAgents.map((agent) => ({
|
||||
...agent,
|
||||
model: normalizeModelId(agent.model),
|
||||
})),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
writeJson(USER_SETTINGS_PATH, next);
|
||||
@@ -162,7 +181,11 @@ export function loadProjectSettings(): ProjectSettings {
|
||||
export function saveProjectSettings(partial: Partial<ProjectSettings>): void {
|
||||
const projectPath = path.join(process.cwd(), ".grok", "settings.json");
|
||||
const current = loadProjectSettings();
|
||||
writeJson(projectPath, { ...current, ...partial });
|
||||
writeJson(projectPath, {
|
||||
...current,
|
||||
...partial,
|
||||
...(partial.model !== undefined ? { model: normalizeModelId(partial.model) } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
export function getApiKey(): string | undefined {
|
||||
@@ -174,11 +197,20 @@ export function getBaseURL(): string {
|
||||
}
|
||||
|
||||
export function getCurrentModel(): string {
|
||||
if (process.env.GROK_MODEL) return process.env.GROK_MODEL;
|
||||
if (process.env.GROK_MODEL) return normalizeModelId(process.env.GROK_MODEL);
|
||||
const project = loadProjectSettings();
|
||||
if (project.model) return project.model;
|
||||
if (project.model) return normalizeModelId(project.model);
|
||||
const user = loadUserSettings();
|
||||
return user.defaultModel || DEFAULT_MODEL;
|
||||
return user.defaultModel ? normalizeModelId(user.defaultModel) : DEFAULT_MODEL;
|
||||
}
|
||||
|
||||
export function getReasoningEffortForModel(modelId: string): ReasoningEffort | undefined {
|
||||
const normalizedModelId = normalizeModelId(modelId);
|
||||
const savedEfforts = loadUserSettings().reasoningEffortByModel ?? {};
|
||||
const effort =
|
||||
savedEfforts[normalizedModelId] ??
|
||||
Object.entries(savedEfforts).find(([savedModelId]) => normalizeModelId(savedModelId) === normalizedModelId)?.[1];
|
||||
return getEffectiveReasoningEffort(normalizedModelId, effort);
|
||||
}
|
||||
|
||||
export function getTelegramBotToken(): string | undefined {
|
||||
|
||||
@@ -10,8 +10,18 @@ describe("parseSubAgentsRawList", () => {
|
||||
|
||||
it("keeps valid entries with known model ids", () => {
|
||||
expect(
|
||||
parseSubAgentsRawList([{ name: "docs", model: "grok-4-1-fast", instruction: "Focus on documentation." }]),
|
||||
).toEqual([{ name: "docs", model: "grok-4-1-fast", instruction: "Focus on documentation." }]);
|
||||
parseSubAgentsRawList([
|
||||
{ name: "docs", model: "grok-4-1-fast-reasoning", instruction: "Focus on documentation." },
|
||||
]),
|
||||
).toEqual([{ name: "docs", model: "grok-4-1-fast-reasoning", instruction: "Focus on documentation." }]);
|
||||
});
|
||||
|
||||
it("normalizes legacy aliases to canonical ids", () => {
|
||||
expect(
|
||||
parseSubAgentsRawList([
|
||||
{ name: "research", model: "x-ai/grok-4.20-multi-agent-beta", instruction: "Focus on research." },
|
||||
]),
|
||||
).toEqual([{ name: "research", model: "grok-4.20-multi-agent-0309", instruction: "Focus on research." }]);
|
||||
});
|
||||
|
||||
it("skips unknown models", () => {
|
||||
@@ -21,10 +31,10 @@ describe("parseSubAgentsRawList", () => {
|
||||
it("skips reserved and empty names", () => {
|
||||
expect(
|
||||
parseSubAgentsRawList([
|
||||
{ name: "general", model: "grok-4-1-fast", instruction: "x" },
|
||||
{ name: "Explore", model: "grok-4-1-fast", instruction: "x" },
|
||||
{ name: "", model: "grok-4-1-fast", instruction: "x" },
|
||||
{ name: " ", model: "grok-4-1-fast", instruction: "x" },
|
||||
{ name: "general", model: "grok-4-1-fast-reasoning", instruction: "x" },
|
||||
{ name: "Explore", model: "grok-4-1-fast-reasoning", instruction: "x" },
|
||||
{ name: "", model: "grok-4-1-fast-reasoning", instruction: "x" },
|
||||
{ name: " ", model: "grok-4-1-fast-reasoning", instruction: "x" },
|
||||
]),
|
||||
).toEqual([]);
|
||||
});
|
||||
@@ -35,7 +45,7 @@ describe("parseSubAgentsRawList", () => {
|
||||
{ name: "Docs", model: "grok-4-1-fast", instruction: "first" },
|
||||
{ name: "docs", model: "grok-code-fast-1", instruction: "second" },
|
||||
]),
|
||||
).toEqual([{ name: "Docs", model: "grok-4-1-fast", instruction: "first" }]);
|
||||
).toEqual([{ name: "Docs", model: "grok-4-1-fast-reasoning", instruction: "first" }]);
|
||||
});
|
||||
|
||||
it("ignores non-object rows", () => {
|
||||
|
||||
Reference in new issue
Block a user