fluxflow-cli 1.19.6 → 1.20.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.
Files changed (2) hide show
  1. package/dist/fluxflow.js +362 -94
  2. package/package.json +1 -1
package/dist/fluxflow.js CHANGED
@@ -1171,7 +1171,7 @@ var init_main_tools = __esm({
1171
1171
  }
1172
1172
  return _isPsAvailable;
1173
1173
  };
1174
- TOOL_PROTOCOL = (mode, osDetected) => `
1174
+ TOOL_PROTOCOL = (mode, osDetected, isMultiModal, aiProvider) => `
1175
1175
  -- TOOL DEFINITIONS --
1176
1176
  Access to internal tools. MUST use the exact syntax on a new line: [tool:functions.ToolName(args)]
1177
1177
  MANDATORY TOOL POLICY:
@@ -1186,7 +1186,7 @@ ${mode === "Flux" ? "- **File Tools >> Code in chat**\n" : ""}
1186
1186
  2. [tool:functions.WebScrape(url="...")]. Proactive use for specific webpage/docs/api
1187
1187
 
1188
1188
  ${mode === "Flux" ? `- PROJECT TOOLS (path = relative to CWD, path separator: '/') -
1189
- 1. [tool:functions.ReadFile(path="...", startLine=number, endLine=number)]. Supports images/docs. User gives image/doc: VIEW FIRST
1189
+ 1. [tool:functions.ReadFile(path="...", startLine=number, endLine=number)]. ${aiProvider !== "Google" ? `${isMultiModal ? `Supports images/docs. User gives image/doc: VIEW FIRST` : `No Multimodal support`}` : `Supports images/docs. User gives image/doc: VIEW FIRST`}
1190
1190
  2. [tool:functions.ReadFolder(path="...")]. Detailed DIR stats
1191
1191
  3. [tool:functions.PatchFile(path="...", replaceContent1="exact string", newContent1="...", ...MAX 8)]. Surgical Patch. **Multiple patch on same file/path? Use replaceContent2, newContent2 etc >>> multiple spams**. Unsure? ReadFile > guessing.
1192
1192
  4. [tool:functions.WriteFile(path="...", content="...")]. Creates/Overwrites. File Exist? PatchFile >>> WriteFile. Verify Imports
@@ -2413,7 +2413,7 @@ ${userMemories}` : "";
2413
2413
  ${parts.join("\n\n")}
2414
2414
  ` : "";
2415
2415
  };
2416
- getSystemInstruction = (profile, thinkingLevel, mode, systemSettings, isMemoryEnabled = true, isFirstPrompt = false) => {
2416
+ getSystemInstruction = (profile, thinkingLevel, mode, systemSettings, isMemoryEnabled = true, isFirstPrompt = false, aiProvider = "Google", isMultiModal = false) => {
2417
2417
  let thinkingConfig = "";
2418
2418
  if (thinkingLevel !== "GEM") {
2419
2419
  let levelKey = thinkingLevel;
@@ -2472,15 +2472,15 @@ Mode: ${mode}${thinkingLevel !== "Fast" ? " (Thinking Mode)" : ""}. ${mode === "
2472
2472
  -- MARKERS --
2473
2473
  - TOOL SYSTEM: [TOOL RESULT] (system priority)
2474
2474
  - SYSTEM NOTIFICATION: [SYSTEM], [METADATA] in user turn
2475
- ${thinkingLevel !== "GEM" ? `
2475
+ ${aiProvider === "Google" ? `${thinkingLevel !== "GEM" ? `
2476
2476
  -- THINKING RULES --
2477
2477
  ${thinkingConfig}
2478
2478
  ${thinkingLevel !== "Fast" ? `
2479
2479
  CRITICAL THINKING POLICY
2480
2480
  - ALWAYS use <think> ... </think> before responding, even with simple queries/greetings
2481
2481
  - ${thinkingLevel === "Low" || thinkingLevel === "Medium" || thinkingLevel === "Fast" ? "C" : "Interrogate approaches adversarially, but c"}ommit once best solution is determined through analysis. Avoid spiraling after reaching decision point
2482
- ` : ""}` : ""}
2483
- ${TOOL_PROTOCOL(mode, osDetected)}
2482
+ ` : ""}` : ""}` : ``}
2483
+ ${TOOL_PROTOCOL(mode, osDetected, isMultiModal, aiProvider)}
2484
2484
  ${projectContextBlock}
2485
2485
  -- MEMORY RULES --
2486
2486
  - Memory: ${isMemoryEnabled ? "Subtly Personalize. Auto Saves" : "OFF. Decline Remembering Memories"}
@@ -4415,6 +4415,7 @@ var init_settings = __esm({
4415
4415
  DEFAULT_SETTINGS = {
4416
4416
  mode: "Flux",
4417
4417
  thinkingLevel: "Medium",
4418
+ aiProvider: "Google",
4418
4419
  activeModel: "gemma-4-31b-it",
4419
4420
  showFullThinking: true,
4420
4421
  apiTier: "Free",
@@ -4922,7 +4923,7 @@ var init_tools = __esm({
4922
4923
  import { GoogleGenAI, ThinkingLevel, HarmBlockThreshold, HarmCategory } from "@google/genai";
4923
4924
  import path16 from "path";
4924
4925
  import fs17 from "fs";
4925
- var client, TERMINATION_SIGNAL, stripAnsi2, signalTermination, TOOL_LABELS2, getToolDetail, runJanitorTask, getActiveToolContext, getContextSafeText, contextSafeReplace, getSanitizedText, detectToolCalls, initAI, consolidatePastMemories, getAIStream;
4926
+ var client, TERMINATION_SIGNAL, stripAnsi2, fetchWithBackoff, getOpenRouterStream, signalTermination, TOOL_LABELS2, getToolDetail, runJanitorTask, getActiveToolContext, getContextSafeText, contextSafeReplace, getSanitizedText, detectToolCalls, initAI, consolidatePastMemories, getAIStream;
4926
4927
  var init_ai = __esm({
4927
4928
  async "src/utils/ai.js"() {
4928
4929
  await init_prompts();
@@ -4940,6 +4941,141 @@ var init_ai = __esm({
4940
4941
  if (typeof str !== "string") return str;
4941
4942
  return str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
4942
4943
  };
4944
+ fetchWithBackoff = async (url, options, retries = 5, delay = 1e3) => {
4945
+ for (let i = 0; i < retries; i++) {
4946
+ try {
4947
+ const response = await fetch(url, options);
4948
+ if (response.ok) return response;
4949
+ if (response.status !== 429 && response.status < 500) return response;
4950
+ } catch (e) {
4951
+ if (i === retries - 1) throw e;
4952
+ }
4953
+ await new Promise((resolve) => setTimeout(resolve, delay * Math.pow(2, i)));
4954
+ }
4955
+ return fetch(url, options);
4956
+ };
4957
+ getOpenRouterStream = async function* (apiKey, model, contents, systemInstruction, thinkingLevel, mode, isMultiModal) {
4958
+ const messages = [];
4959
+ if (systemInstruction) {
4960
+ messages.push({ role: "system", content: systemInstruction });
4961
+ }
4962
+ for (const content of contents) {
4963
+ const role = content.role === "user" ? "user" : "assistant";
4964
+ const msgContent = [];
4965
+ if (Array.isArray(content.parts)) {
4966
+ for (const part of content.parts) {
4967
+ if (part.text) {
4968
+ msgContent.push({ type: "text", text: part.text });
4969
+ } else if (part.inlineData && isMultiModal) {
4970
+ const mimeType = part.inlineData.mimeType;
4971
+ const data = part.inlineData.data;
4972
+ const isImage = mimeType.startsWith("image/");
4973
+ if (isImage) {
4974
+ msgContent.push({
4975
+ type: "image_url",
4976
+ image_url: {
4977
+ url: `data:${mimeType};base64,${data}`
4978
+ }
4979
+ });
4980
+ } else {
4981
+ msgContent.push({
4982
+ type: "file",
4983
+ file: {
4984
+ filename: part.filename || "file",
4985
+ file_data: `data:${mimeType};base64,${data}`
4986
+ }
4987
+ });
4988
+ }
4989
+ }
4990
+ }
4991
+ } else {
4992
+ const text = content.text || "";
4993
+ if (text) msgContent.push({ type: "text", text });
4994
+ }
4995
+ messages.push({
4996
+ role,
4997
+ content: msgContent.length === 1 && msgContent[0].type === "text" ? msgContent[0].text : msgContent
4998
+ });
4999
+ }
5000
+ const reasoningEffortMap = {
5001
+ "Low": "low",
5002
+ "Medium": "medium",
5003
+ "High": "high",
5004
+ "xHigh": "high"
5005
+ };
5006
+ const requestPayload = {
5007
+ model,
5008
+ messages,
5009
+ stream: true,
5010
+ temperature: mode === "Flux" ? 1 : 1.4
5011
+ };
5012
+ const effort = reasoningEffortMap[thinkingLevel];
5013
+ if (effort && thinkingLevel !== "Fast") {
5014
+ requestPayload.reasoning_effort = effort;
5015
+ }
5016
+ const response = await fetchWithBackoff("https://openrouter.ai/api/v1/chat/completions", {
5017
+ method: "POST",
5018
+ headers: {
5019
+ "Authorization": `Bearer ${apiKey}`,
5020
+ "Content-Type": "application/json",
5021
+ "X-Title": "FluxFlow CLI",
5022
+ "X-Cache": "true"
5023
+ },
5024
+ body: JSON.stringify(requestPayload)
5025
+ });
5026
+ if (!response.ok) {
5027
+ const errData = await response.json().catch(() => ({}));
5028
+ throw new Error(`OpenRouter Error (${response.status}): ${errData.error?.message || response.statusText}`);
5029
+ }
5030
+ const reader = response.body.getReader();
5031
+ const decoder = new TextDecoder();
5032
+ let buffer = "";
5033
+ while (true) {
5034
+ const { done, value } = await reader.read();
5035
+ if (done) break;
5036
+ buffer += decoder.decode(value, { stream: true });
5037
+ const lines = buffer.split("\n");
5038
+ buffer = lines.pop();
5039
+ for (const line of lines) {
5040
+ const cleanLine = line.trim();
5041
+ if (!cleanLine || !cleanLine.startsWith("data: ")) continue;
5042
+ if (cleanLine === "data: [DONE]") break;
5043
+ try {
5044
+ const json = JSON.parse(cleanLine.substring(6));
5045
+ const delta = json.choices?.[0]?.delta;
5046
+ const usage = json.usage;
5047
+ let usageMetadata = null;
5048
+ if (usage) {
5049
+ usageMetadata = {
5050
+ totalTokenCount: usage.total_tokens || usage.prompt_tokens + usage.completion_tokens,
5051
+ promptTokenCount: usage.prompt_tokens || 0,
5052
+ candidatesTokenCount: usage.completion_tokens || 0,
5053
+ cachedContentTokenCount: usage.prompt_tokens_details?.cached_tokens || 0,
5054
+ thoughtsTokenCount: usage.completion_tokens_details?.reasoning_tokens || 0
5055
+ };
5056
+ }
5057
+ if (delta) {
5058
+ const thought = delta.reasoning || (delta.reasoning_details ? delta.reasoning_details.map((d) => d.text).join("") : null);
5059
+ const parts = [];
5060
+ if (thought) parts.push({ text: thought, thought: true });
5061
+ if (delta.content) parts.push({ text: delta.content });
5062
+ if (parts.length > 0 || usageMetadata) {
5063
+ yield {
5064
+ candidates: parts.length > 0 ? [{ content: { parts } }] : [],
5065
+ usageMetadata
5066
+ };
5067
+ }
5068
+ } else if (usageMetadata) {
5069
+ yield {
5070
+ candidates: [],
5071
+ usageMetadata
5072
+ };
5073
+ }
5074
+ } catch (e) {
5075
+ }
5076
+ }
5077
+ }
5078
+ };
4943
5079
  signalTermination = () => {
4944
5080
  TERMINATION_SIGNAL = true;
4945
5081
  };
@@ -4974,7 +5110,7 @@ var init_ai = __esm({
4974
5110
  const USER_CONTEXT_LENGTH = 4 * (1024 * 2);
4975
5111
  const AGENT_CONTEXT_LENGTH = 4 * (1024 * 8);
4976
5112
  const { onStatus, onMemoryUpdated, onBackgroundIncrement } = callbacks;
4977
- const { profile, thinkingLevel, mode, janitorModel, chatId, systemSettings, sessionStats } = settings;
5113
+ const { profile, thinkingLevel, mode, janitorModel, chatId, systemSettings, sessionStats, aiProvider = "Google", apiKey } = settings;
4978
5114
  const isMemoryEnabled = systemSettings?.memory !== false;
4979
5115
  const persistentStorage = readEncryptedJson(MEMORIES_FILE, []);
4980
5116
  const janitorUserMemories = persistentStorage.map((m) => `- [${m.id}]: ${m.memory}`).join("\n");
@@ -5031,25 +5167,41 @@ ${originalTextProcessed.length > USER_CONTEXT_LENGTH ? "... (truncated) ...\n\n"
5031
5167
  (_, reject) => setTimeout(() => reject(new Error("JANITOR_TIMEOUT")), 6e4)
5032
5168
  );
5033
5169
  const streamPromise = (async () => {
5034
- const stream = await client.models.generateContentStream({
5035
- model: janitorModel || "gemma-4-26b-a4b-it",
5036
- contents: janitorContents,
5037
- config: {
5038
- systemInstruction: janitorPrompt,
5039
- maxOutputTokens: 512,
5040
- temperature: 0.3,
5041
- safetySettings: [
5042
- { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE },
5043
- { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_NONE },
5044
- { category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold: HarmBlockThreshold.BLOCK_NONE },
5045
- { category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_NONE }
5046
- ],
5047
- thinkingConfig: { includeThoughts: false, thinkingLevel: ThinkingLevel.MINIMAL }
5048
- }
5049
- });
5050
- const iterator2 = stream[Symbol.asyncIterator]();
5051
- const firstResult2 = await iterator2.next();
5052
- return { iterator: iterator2, firstResult: firstResult2 };
5170
+ if (aiProvider === "OpenRouter") {
5171
+ const janitorOpenRouterModel = "google/gemma-4-26b-a4b-it:free";
5172
+ const stream = getOpenRouterStream(
5173
+ apiKey,
5174
+ janitorOpenRouterModel,
5175
+ janitorContents,
5176
+ janitorPrompt,
5177
+ "Fast",
5178
+ // Janitor always minimal
5179
+ mode
5180
+ );
5181
+ const iterator2 = stream[Symbol.asyncIterator]();
5182
+ const firstResult2 = await iterator2.next();
5183
+ return { iterator: iterator2, firstResult: firstResult2 };
5184
+ } else {
5185
+ const stream = await client.models.generateContentStream({
5186
+ model: janitorModel || "gemma-4-26b-a4b-it",
5187
+ contents: janitorContents,
5188
+ config: {
5189
+ systemInstruction: janitorPrompt,
5190
+ maxOutputTokens: 512,
5191
+ temperature: 0.3,
5192
+ safetySettings: [
5193
+ { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE },
5194
+ { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_NONE },
5195
+ { category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold: HarmBlockThreshold.BLOCK_NONE },
5196
+ { category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_NONE }
5197
+ ],
5198
+ thinkingConfig: { includeThoughts: false, thinkingLevel: ThinkingLevel.MINIMAL }
5199
+ }
5200
+ });
5201
+ const iterator2 = stream[Symbol.asyncIterator]();
5202
+ const firstResult2 = await iterator2.next();
5203
+ return { iterator: iterator2, firstResult: firstResult2 };
5204
+ }
5053
5205
  })();
5054
5206
  const { iterator, firstResult } = await Promise.race([streamPromise, timeoutPromise]);
5055
5207
  let { value: firstChunk, done: firstDone } = firstResult;
@@ -5467,8 +5619,8 @@ ${newMemoryListStr}
5467
5619
  }
5468
5620
  };
5469
5621
  getAIStream = async function* (modelName, history, settings, steeringCallback, versionFluxflow2) {
5470
- if (!client) throw new Error("AI not initialized");
5471
- const { profile, thinkingLevel, mode, janitorModel, chatId, systemSettings, sessionStats } = settings;
5622
+ const { profile, thinkingLevel, mode, janitorModel, chatId, systemSettings, sessionStats, aiProvider = "Google", isMultiModal } = settings;
5623
+ if (!client && aiProvider === "Google") throw new Error("AI not initialized");
5472
5624
  const isMemoryEnabled = systemSettings?.memory !== false;
5473
5625
  const originalText = history[history.length - 1].text;
5474
5626
  const summariesFile = path16.join(SECRET_DIR, "chat-summaries.json");
@@ -5872,7 +6024,7 @@ CWD: ${process.cwd()}${cwdMismatch ? ` (WARNING: CWD Mismatch! Previous Path: ${
5872
6024
  ${dirStructure}
5873
6025
  ${summaryBlock}
5874
6026
  ${memoryPrompt}
5875
- ${thinkingLevel != "Fast" ? `${modelName.toLowerCase().startsWith("gemma") ? "[SYSTEM] **STRICTLY FOLLOW THINKING POLICY AS CRITICAL PRIORITY. DO NOT START A RESPONSE WITHOUT <think> ... </think>**\n" : ""}` : ""}[USER] ${agentText.replace(/\s*\[Prompted on:.*?\]/g, "").trim()}`.trim();
6027
+ ${thinkingLevel != "Fast" && aiProvider === "Google" ? `${modelName.toLowerCase().startsWith("gemma") ? "[SYSTEM] **STRICTLY FOLLOW THINKING POLICY AS CRITICAL PRIORITY. DO NOT START A RESPONSE WITHOUT <think> ... </think>**\n" : ""}` : ""}[USER] ${agentText.replace(/\s*\[Prompted on:.*?\]/g, "").trim()}`.trim();
5876
6028
  modifiedHistory.push({ role: "user", text: firstUserMsg });
5877
6029
  let lastUsage = null;
5878
6030
  const MAX_LOOPS = mode === "Flux" ? 70 : 7;
@@ -5903,7 +6055,7 @@ ${thinkingLevel != "Fast" ? `${modelName.toLowerCase().startsWith("gemma") ? "[S
5903
6055
 
5904
6056
  [STEERING HINT]: ${hint}`;
5905
6057
  } else {
5906
- modifiedHistory.push({ role: "user", text: `${thinkingLevel != "Fast" ? `${modelName.toLowerCase().startsWith("gemma") ? "[SYSTEM] **STRICTLY FOLLOW THINKING POLICY AS CRITICAL PRIORITY. DO NOT START A RESPONSE WITHOUT <think> ... </think>**\n" : ""}` : ""}[STEERING HINT]: ${hint}` });
6058
+ modifiedHistory.push({ role: "user", text: `${thinkingLevel != "Fast" && aiProvider === "Google" ? `${modelName.toLowerCase().startsWith("gemma") ? "[SYSTEM] **STRICTLY FOLLOW THINKING POLICY AS CRITICAL PRIORITY. DO NOT START A RESPONSE WITHOUT <think> ... </think>**\n" : ""}` : ""}[STEERING HINT]: ${hint}` });
5907
6059
  }
5908
6060
  yield { type: "status", content: "Steering Hint Injected." };
5909
6061
  }
@@ -5973,12 +6125,12 @@ ${thinkingLevel != "Fast" ? `${modelName.toLowerCase().startsWith("gemma") ? "[S
5973
6125
  } else if (retryCount > 0) {
5974
6126
  yield { type: "model_update", content: null };
5975
6127
  }
5976
- currentSystemInstruction = getSystemInstruction(profile, !(targetModel || "gemma").toLowerCase().startsWith("gemma") ? "GEM" : thinkingLevel, mode, systemSettings, isMemoryEnabled, isFirstPrompt);
6128
+ currentSystemInstruction = getSystemInstruction(profile, !(targetModel || "gemma").toLowerCase().startsWith("gemma") ? "GEM" : thinkingLevel, mode, systemSettings, isMemoryEnabled, isFirstPrompt, aiProvider, isMultiModal);
5977
6129
  const isGemma = modelName && modelName.toLowerCase().startsWith("gemma");
5978
6130
  const lastUserMsg = contents[contents.length - 1];
5979
6131
  if (isGemma) {
5980
6132
  const jitInstruction = `
5981
- [SYSTEM] Tool result received. Analyze output and proceed with your turn${thinkingLevel != "Fast" ? `. **STRICTLY MAINTAIN THINKING POLICY. DO NOT START A RESPONSE WITHOUT <think> ... </think>}**` : ""}`;
6133
+ [SYSTEM] Tool result received. Analyze output and proceed with your turn${thinkingLevel != "Fast" && aiProvider === "Google" ? `. **STRICTLY MAINTAIN THINKING POLICY. DO NOT START A RESPONSE WITHOUT <think> ... </think>}**` : ""}`;
5982
6134
  if (lastUserMsg && lastUserMsg.role === "user" && lastUserMsg.parts?.[0]?.text?.startsWith("[TOOL RESULT]")) {
5983
6135
  lastUserMsg.parts[0].text += jitInstruction;
5984
6136
  }
@@ -5992,57 +6144,69 @@ ${thinkingLevel != "Fast" ? `${modelName.toLowerCase().startsWith("gemma") ? "[S
5992
6144
  }
5993
6145
  }
5994
6146
  let activeContents = contents;
5995
- stream = await client.models.generateContentStream({
5996
- model: targetModel || "gemma-4-31b-it",
5997
- contents: activeContents,
5998
- config: {
5999
- systemInstruction: currentSystemInstruction,
6000
- temperature: mode === "Flux" ? 1 : 1.4,
6001
- maxOutputTokens: 32768,
6002
- mediaResolution: "MEDIA_RESOLUTION_MEDIUM",
6003
- safetySettings: [
6004
- { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE },
6005
- { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_NONE },
6006
- { category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold: HarmBlockThreshold.BLOCK_NONE },
6007
- { category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_NONE }
6008
- ],
6009
- thinkingConfig: (() => {
6010
- const modelLower = (targetModel || "").toLowerCase();
6011
- const isGemma4 = modelLower.includes("gemma-4") || modelLower.startsWith("gemma");
6012
- const isGemini3 = modelLower.includes("gemini-3");
6013
- if (isGemma4 || isGemini3) {
6014
- if (isGemma4) {
6015
- return { includeThoughts: false, thinkingLevel: ThinkingLevel.MINIMAL };
6016
- }
6017
- return {
6018
- includeThoughts: true,
6019
- thinkingLevel: {
6020
- "Fast": modelLower.includes("pro") ? ThinkingLevel.LOW : ThinkingLevel.MINIMAL,
6021
- "Low": ThinkingLevel.LOW,
6022
- "Medium": ThinkingLevel.MEDIUM,
6023
- "High": ThinkingLevel.HIGH,
6024
- "xHigh": ThinkingLevel.HIGH
6025
- }[thinkingLevel] || ThinkingLevel.MEDIUM
6026
- };
6027
- } else {
6028
- const budget = {
6029
- "Fast": -1,
6030
- "Low": 512,
6031
- "Medium": 2048,
6032
- "High": 16384,
6033
- "xHigh": 24576
6034
- }[thinkingLevel] || 2048;
6035
- if (budget === -1) {
6036
- return { includeThoughts: false };
6147
+ if (aiProvider === "OpenRouter") {
6148
+ stream = getOpenRouterStream(
6149
+ settings.apiKey,
6150
+ targetModel,
6151
+ activeContents,
6152
+ currentSystemInstruction,
6153
+ thinkingLevel,
6154
+ mode,
6155
+ isMultiModal
6156
+ );
6157
+ } else {
6158
+ stream = await client.models.generateContentStream({
6159
+ model: targetModel || "gemma-4-31b-it",
6160
+ contents: activeContents,
6161
+ config: {
6162
+ systemInstruction: currentSystemInstruction,
6163
+ temperature: mode === "Flux" ? 1 : 1.4,
6164
+ maxOutputTokens: 32768,
6165
+ mediaResolution: "MEDIA_RESOLUTION_MEDIUM",
6166
+ safetySettings: [
6167
+ { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE },
6168
+ { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_NONE },
6169
+ { category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold: HarmBlockThreshold.BLOCK_NONE },
6170
+ { category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_NONE }
6171
+ ],
6172
+ thinkingConfig: (() => {
6173
+ const modelLower = (targetModel || "").toLowerCase();
6174
+ const isGemma4 = modelLower.includes("gemma-4") || modelLower.startsWith("gemma");
6175
+ const isGemini3 = modelLower.includes("gemini-3");
6176
+ if (isGemma4 || isGemini3) {
6177
+ if (isGemma4) {
6178
+ return { includeThoughts: false, thinkingLevel: ThinkingLevel.MINIMAL };
6179
+ }
6180
+ return {
6181
+ includeThoughts: true,
6182
+ thinkingLevel: {
6183
+ "Fast": modelLower.includes("pro") ? ThinkingLevel.LOW : ThinkingLevel.MINIMAL,
6184
+ "Low": ThinkingLevel.LOW,
6185
+ "Medium": ThinkingLevel.MEDIUM,
6186
+ "High": ThinkingLevel.HIGH,
6187
+ "xHigh": ThinkingLevel.HIGH
6188
+ }[thinkingLevel] || ThinkingLevel.MEDIUM
6189
+ };
6190
+ } else {
6191
+ const budget = {
6192
+ "Fast": 0,
6193
+ "Low": 512,
6194
+ "Medium": 2048,
6195
+ "High": 16384,
6196
+ "xHigh": 24576
6197
+ }[thinkingLevel] || 2048;
6198
+ if (budget === 0) {
6199
+ return { includeThoughts: false };
6200
+ }
6201
+ return {
6202
+ includeThoughts: true,
6203
+ thinkingBudget: budget
6204
+ };
6037
6205
  }
6038
- return {
6039
- includeThoughts: true,
6040
- thinkingBudget: budget
6041
- };
6042
- }
6043
- })()
6044
- }
6045
- });
6206
+ })()
6207
+ }
6208
+ });
6209
+ }
6046
6210
  turnText = "";
6047
6211
  lastToolSniffed = null;
6048
6212
  lastToolEventTime = null;
@@ -6595,7 +6759,9 @@ ${boxBottom}` };
6595
6759
  toolCallPointer++;
6596
6760
  }
6597
6761
  }
6598
- lastUsage = chunk.usageMetadata;
6762
+ if (chunk.usageMetadata) {
6763
+ lastUsage = chunk.usageMetadata;
6764
+ }
6599
6765
  if (lastUsage) {
6600
6766
  yield { type: "liveTokens", content: lastUsage.totalTokenCount };
6601
6767
  }
@@ -7460,6 +7626,8 @@ function App({ args = [] }) {
7460
7626
  };
7461
7627
  }, [stdout]);
7462
7628
  const [thinkingLevel, setThinkingLevel] = useState10("Medium");
7629
+ const [aiProvider, setAiProvider] = useState10("Google");
7630
+ const [setupStep, setSetupStep] = useState10(0);
7463
7631
  const [latestVer, setLatestVer] = useState10(null);
7464
7632
  const [showFullThinking, setShowFullThinking] = useState10(false);
7465
7633
  const [activeModel, setActiveModel] = useState10("gemma-4-31b-it");
@@ -7717,6 +7885,11 @@ function App({ args = [] }) {
7717
7885
  } else if (activeView !== "chat" && activeView !== "settings") {
7718
7886
  setActiveView("chat");
7719
7887
  } else {
7888
+ if (!apiKey && setupStep === 1) {
7889
+ setSetupStep(0);
7890
+ setTempKey("");
7891
+ return;
7892
+ }
7720
7893
  setEscPressCount((prev) => {
7721
7894
  const nextCount = prev + 1;
7722
7895
  if (nextCount === 1) {
@@ -7809,6 +7982,7 @@ function App({ args = [] }) {
7809
7982
  } else {
7810
7983
  setThinkingLevel(saved.thinkingLevel);
7811
7984
  }
7985
+ setAiProvider(saved.aiProvider || "Google");
7812
7986
  persistedModelRef.current = saved.activeModel;
7813
7987
  if (parsedArgs.model) {
7814
7988
  setActiveModel(parsedArgs.model);
@@ -7919,6 +8093,7 @@ function App({ args = [] }) {
7919
8093
  saveSettings({
7920
8094
  mode,
7921
8095
  thinkingLevel,
8096
+ aiProvider,
7922
8097
  activeModel: modelToSave || activeModel,
7923
8098
  showFullThinking,
7924
8099
  systemSettings,
@@ -7927,16 +8102,17 @@ function App({ args = [] }) {
7927
8102
  apiTier
7928
8103
  });
7929
8104
  }
7930
- }, [mode, thinkingLevel, activeModel, showFullThinking, systemSettings, profileData, imageSettings, isInitializing, parsedArgs, apiTier]);
8105
+ }, [mode, thinkingLevel, aiProvider, activeModel, showFullThinking, systemSettings, profileData, imageSettings, isInitializing, parsedArgs, apiTier]);
7931
8106
  const handleSetup = async (val) => {
7932
8107
  const key = val.trim();
7933
- if (key.length >= 30) {
8108
+ const minLength = aiProvider === "OpenRouter" ? 10 : 30;
8109
+ if (key.length >= minLength) {
7934
8110
  await saveAPIKey(key);
7935
8111
  setApiKey(key);
7936
8112
  initAI(key);
7937
- setMessages((prev) => [...prev, { role: "system", text: "\u2705 API Key saved successfully! Initialization complete.", isMeta: true }]);
8113
+ setMessages((prev) => [...prev, { role: "system", text: `\u2705 ${aiProvider} API Key saved successfully! Initialization complete.`, isMeta: true }]);
7938
8114
  } else {
7939
- setMessages((prev) => [...prev, { role: "system", text: `\u274C INVALID KEY: Gemini API keys must be at least 30 characters.`, isMeta: true }]);
8115
+ setMessages((prev) => [...prev, { role: "system", text: `\u274C INVALID KEY: ${aiProvider} API keys must be at least ${minLength} characters.`, isMeta: true }]);
7940
8116
  setTempKey("");
7941
8117
  }
7942
8118
  };
@@ -8037,7 +8213,81 @@ function App({ args = [] }) {
8037
8213
  {
8038
8214
  cmd: "/model",
8039
8215
  desc: "Switch Model for Agent",
8040
- subs: apiTier === "Free" ? [
8216
+ subs: aiProvider === "OpenRouter" ? apiTier === "Free" ? [
8217
+ {
8218
+ cmd: "google/gemma-4-31b-it:free",
8219
+ desc: "Multimodal"
8220
+ },
8221
+ {
8222
+ cmd: "moonshotai/kimi-k2.6:free",
8223
+ desc: "Multimodal"
8224
+ },
8225
+ {
8226
+ cmd: "qwen/qwen3-coder:free",
8227
+ desc: ""
8228
+ },
8229
+ {
8230
+ cmd: "z-ai/glm-4.5-air:free",
8231
+ desc: ""
8232
+ }
8233
+ ] : [
8234
+ {
8235
+ cmd: "google/gemini-3.5-flash",
8236
+ desc: "Multimodal"
8237
+ },
8238
+ {
8239
+ cmd: "qwen/qwen3.7-plus",
8240
+ desc: "Multimodal"
8241
+ },
8242
+ {
8243
+ cmd: "minimax/minimax-m3",
8244
+ desc: "Multimodal"
8245
+ },
8246
+ {
8247
+ cmd: "anthropic/claude-sonnet-4.5",
8248
+ desc: "Multimodal"
8249
+ },
8250
+ {
8251
+ cmd: "anthropic/claude-opus-4.6",
8252
+ desc: "Multimodal"
8253
+ },
8254
+ {
8255
+ cmd: "anthropic/claude-opus-4.8",
8256
+ desc: "Multimodal"
8257
+ },
8258
+ {
8259
+ cmd: "deepseek/deepseek-v4-pro",
8260
+ desc: ""
8261
+ },
8262
+ {
8263
+ cmd: "deepseek/deepseek-v4-flash",
8264
+ desc: ""
8265
+ },
8266
+ {
8267
+ cmd: "xiaomi/mimo-v2.5-pro",
8268
+ desc: ""
8269
+ },
8270
+ {
8271
+ cmd: "z-ai/glm-5",
8272
+ desc: ""
8273
+ },
8274
+ {
8275
+ cmd: "openai/gpt-5.2-codex",
8276
+ desc: "Multimodal"
8277
+ },
8278
+ {
8279
+ cmd: "openai/gpt-5.2-pro",
8280
+ desc: "Multimodal"
8281
+ },
8282
+ {
8283
+ cmd: "openai/gpt-5.5-pro",
8284
+ desc: "Multimodal"
8285
+ },
8286
+ {
8287
+ cmd: "moonshotai/kimi-k2.6",
8288
+ desc: "Multimodal"
8289
+ }
8290
+ ] : apiTier === "Free" ? [
8041
8291
  {
8042
8292
  cmd: "gemma-4-31b-it",
8043
8293
  desc: "Standard Default"
@@ -8388,7 +8638,7 @@ ${hintText}`, color: "magenta" }];
8388
8638
  case "/model": {
8389
8639
  if (parts[1]) {
8390
8640
  const mod = parts.slice(1).join(" ");
8391
- if (mod === "gemma-4-31b-it" && apiTier !== "Free") {
8641
+ if (mod === "gemma-4-31b-it" && apiTier !== "Free" && aiProvider === "Google") {
8392
8642
  setMessages((prev) => {
8393
8643
  setCompletedIndex(prev.length + 1);
8394
8644
  return [...prev, {
@@ -8697,6 +8947,9 @@ ${timestamp}` };
8697
8947
  text
8698
8948
  });
8699
8949
  });
8950
+ const modelCmd = COMMANDS.find((c) => c.cmd === "/model");
8951
+ const currentModelObj = modelCmd?.subs?.find((s) => s.cmd === activeModel);
8952
+ const isMultiModal = currentModelObj?.desc?.toLowerCase().includes("multimodal");
8700
8953
  const stream = getAIStream(
8701
8954
  activeModel,
8702
8955
  cleanHistoryForAI,
@@ -8708,6 +8961,9 @@ ${timestamp}` };
8708
8961
  janitorModel,
8709
8962
  sessionStats,
8710
8963
  chatId,
8964
+ isMultiModal,
8965
+ aiProvider,
8966
+ apiKey,
8711
8967
  cols: terminalSize.columns - 6,
8712
8968
  rows: 30,
8713
8969
  onExecStart: (cmd) => {
@@ -8877,7 +9133,7 @@ Selection: ${val}`,
8877
9133
  setIsProcessing(false);
8878
9134
  hasFiredJanitor = true;
8879
9135
  runJanitorTask(
8880
- { profile: profileData, thinkingLevel, mode, janitorModel, chatId, systemSettings, sessionStats },
9136
+ { profile: profileData, thinkingLevel, mode, janitorModel, chatId, systemSettings, sessionStats, aiProvider, apiKey },
8881
9137
  packet.data.agentText,
8882
9138
  packet.data.fullAgentTextRaw,
8883
9139
  packet.data.history,
@@ -9677,7 +9933,19 @@ Selection: ${val}`,
9677
9933
  showFullThinking,
9678
9934
  columns: Math.max(20, (stdout?.columns || 80) - 1)
9679
9935
  }
9680
- ), activeCommand && /* @__PURE__ */ React13.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React13.createElement(TerminalBox, { command: activeCommand, output: execOutput, isFocused: isTerminalFocused, isPty: isActiveCommandPty }))), isInitializing ? /* @__PURE__ */ React13.createElement(Box13, { borderStyle: "double", borderColor: "magenta", padding: 1, flexShrink: 0 }, /* @__PURE__ */ React13.createElement(Text13, { color: "magenta" }, "\u{1F30A} Starting Flux Flow...")) : !apiKey ? /* @__PURE__ */ React13.createElement(Box13, { borderStyle: "round", borderColor: "gray", padding: 0, flexDirection: "column", flexShrink: 0, width: "100%" }, /* @__PURE__ */ React13.createElement(Box13, { paddingX: 1, marginBottom: 1 }, /* @__PURE__ */ React13.createElement(Text13, { color: "yellow", bold: true }, "\u{1F511}", emojiSpace(2), "API KEY REQUIRED")), /* @__PURE__ */ React13.createElement(Box13, { paddingX: 1, flexDirection: "column" }, /* @__PURE__ */ React13.createElement(Text13, null, "Please enter your Gemini API Key to initialize the agent (If billing is enabled set Tier to paid in /settings \u2192 other \u2192 API Tier)."), /* @__PURE__ */ React13.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React13.createElement(Text13, { color: "cyan", bold: true }, "\u{1F4A0} "), /* @__PURE__ */ React13.createElement(
9936
+ ), activeCommand && /* @__PURE__ */ React13.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React13.createElement(TerminalBox, { command: activeCommand, output: execOutput, isFocused: isTerminalFocused, isPty: isActiveCommandPty }))), isInitializing ? /* @__PURE__ */ React13.createElement(Box13, { borderStyle: "double", borderColor: "magenta", padding: 1, flexShrink: 0 }, /* @__PURE__ */ React13.createElement(Text13, { color: "magenta" }, "\u{1F30A} Starting Flux Flow...")) : !apiKey ? /* @__PURE__ */ React13.createElement(Box13, { borderStyle: "round", borderColor: "gray", padding: 0, flexDirection: "column", flexShrink: 0, width: "100%" }, /* @__PURE__ */ React13.createElement(Box13, { paddingX: 1, marginBottom: 1 }, /* @__PURE__ */ React13.createElement(Text13, { color: "yellow", bold: true }, "\u{1F511}", emojiSpace(2), "API KEY REQUIRED")), /* @__PURE__ */ React13.createElement(Box13, { paddingX: 1, flexDirection: "column" }, setupStep === 0 ? /* @__PURE__ */ React13.createElement(React13.Fragment, null, /* @__PURE__ */ React13.createElement(Text13, null, "Select your Preferred Provider:"), /* @__PURE__ */ React13.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React13.createElement(
9937
+ CommandMenu,
9938
+ {
9939
+ items: [
9940
+ { label: "Google (Daily Free Quota)", value: "Google" },
9941
+ { label: "OpenRouter (Daily Free Quota) [EXPERIMENTAL & UNSTABLE]", value: "OpenRouter" }
9942
+ ],
9943
+ onSelect: (item) => {
9944
+ setAiProvider(item.value);
9945
+ setSetupStep(1);
9946
+ }
9947
+ }
9948
+ ))) : /* @__PURE__ */ React13.createElement(React13.Fragment, null, /* @__PURE__ */ React13.createElement(Text13, null, "Please enter your ", aiProvider, " API Key to initialize the agent (If billing is enabled set Tier to paid in /settings \u2192 other \u2192 API Tier)."), /* @__PURE__ */ React13.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React13.createElement(Text13, { color: "cyan", bold: true }, "\u{1F4A0} "), /* @__PURE__ */ React13.createElement(
9681
9949
  TextInput4,
9682
9950
  {
9683
9951
  value: tempKey,
@@ -9685,7 +9953,7 @@ Selection: ${val}`,
9685
9953
  onSubmit: handleSetup,
9686
9954
  mask: "*"
9687
9955
  }
9688
- ))), /* @__PURE__ */ React13.createElement(Box13, { paddingX: 1, marginTop: 1 }, /* @__PURE__ */ React13.createElement(Text13, { color: "gray", dimColor: true, italic: true }, "(Press Enter to confirm and initialize)"))) : renderActiveView(), confirmExit && /* @__PURE__ */ React13.createElement(Box13, { borderStyle: "round", borderColor: "red", paddingX: 2, marginY: 0, width: "100%" }, /* @__PURE__ */ React13.createElement(Text13, { color: "red", bold: true }, "\u{1F534} EXIT CONFIRMATION: "), /* @__PURE__ */ React13.createElement(Text13, { color: "white" }, "Press "), /* @__PURE__ */ React13.createElement(Text13, { color: "red", bold: true }, "CTRL + C"), /* @__PURE__ */ React13.createElement(Text13, { color: "white" }, " again to exit (", exitCountdown, "s). Press "), /* @__PURE__ */ React13.createElement(Text13, { color: "cyan", bold: true }, "ESC"), /* @__PURE__ */ React13.createElement(Text13, { color: "white" }, " to cancel.")), /* @__PURE__ */ React13.createElement(Box13, { flexShrink: 0, width: "100%" }, /* @__PURE__ */ React13.createElement(
9956
+ )), /* @__PURE__ */ React13.createElement(Box13, { marginTop: 1 }, /* @__PURE__ */ React13.createElement(Text13, { color: "gray", italic: true }, "(Press ESC to go back to provider selection)")))), /* @__PURE__ */ React13.createElement(Box13, { paddingX: 1, marginTop: 1 }, /* @__PURE__ */ React13.createElement(Text13, { color: "gray", dimColor: true, italic: true }, setupStep === 0 ? "(Use arrows to select and Enter to confirm)" : "(Press Enter to confirm and initialize)"))) : renderActiveView(), confirmExit && /* @__PURE__ */ React13.createElement(Box13, { borderStyle: "round", borderColor: "red", paddingX: 2, marginY: 0, width: "100%" }, /* @__PURE__ */ React13.createElement(Text13, { color: "red", bold: true }, "\u{1F534} EXIT CONFIRMATION: "), /* @__PURE__ */ React13.createElement(Text13, { color: "white" }, "Press "), /* @__PURE__ */ React13.createElement(Text13, { color: "red", bold: true }, "CTRL + C"), /* @__PURE__ */ React13.createElement(Text13, { color: "white" }, " again to exit (", exitCountdown, "s). Press "), /* @__PURE__ */ React13.createElement(Text13, { color: "cyan", bold: true }, "ESC"), /* @__PURE__ */ React13.createElement(Text13, { color: "white" }, " to cancel.")), /* @__PURE__ */ React13.createElement(Box13, { flexShrink: 0, width: "100%" }, /* @__PURE__ */ React13.createElement(
9689
9957
  StatusBar_default,
9690
9958
  {
9691
9959
  mode,
@@ -9733,7 +10001,7 @@ Selection: ${val}`,
9733
10001
  paddingX: 1
9734
10002
  },
9735
10003
  /* @__PURE__ */ React13.createElement(Box13, { width: 3 }, /* @__PURE__ */ React13.createElement(Text13, { color: isActive ? "cyan" : "gray", bold: isActive }, isActive ? " \u276F" : " ")),
9736
- /* @__PURE__ */ React13.createElement(Box13, { width: 32 }, /* @__PURE__ */ React13.createElement(
10004
+ /* @__PURE__ */ React13.createElement(Box13, { width: 55 }, /* @__PURE__ */ React13.createElement(
9737
10005
  Text13,
9738
10006
  {
9739
10007
  color: isGemmaDisabled ? "gray" : isActive ? "yellow" : "white",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluxflow-cli",
3
- "version": "1.19.6",
3
+ "version": "1.20.0",
4
4
  "date": "2026-06-05",
5
5
  "description": "A high-fidelity agentic terminal assistant for the Flux Era.",
6
6
  "keywords": [