Skip to content

Commit fd6bcad

Browse files
committed
Integrated iFlow LLM provider
1 parent e28bb23 commit fd6bcad

File tree

6 files changed

+326
-6
lines changed

6 files changed

+326
-6
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ The agents aren't following predefined scripts. They're operating off natural la
2727
**You need:**
2828
- Minecraft 1.20.1 with Forge
2929
- Java 17
30-
- An LLM API key (LongCat, DeepSeek, OpenAI GPT-5, Gemini 3, or Groq)
30+
- An LLM API key (LongCat, iFlow, DeepSeek, OpenAI GPT-5, Gemini 3, or Groq)
3131

3232
**Installation:**
3333
1. Download the JAR from releases
@@ -83,7 +83,7 @@ Each Steve runs an autonomous agent loop that processes natural language command
8383
### Core Components
8484

8585
**LLM Integration** (`com.steve.ai.llm`)
86-
- **Multi-Provider Support**: Pluggable clients for LongCat, DeepSeek, OpenAI, Gemini, and Groq.
86+
- **Multi-Provider Support**: Pluggable clients for LongCat, iFlow, DeepSeek, OpenAI, Gemini, and Groq.
8787
- **Resilient Clients**: Async implementations with caching, retries, and circuit breaker patterns.
8888
- **TaskPlanner**: Orchestrates LLM calls with context (conversation history, world state, Steve capabilities)
8989
- **PromptBuilder**: Constructs prompts with available actions, examples, and formatting instructions
@@ -236,7 +236,7 @@ Edit `config/steve-common.toml`. Each provider now has its own section for bette
236236

237237
```toml
238238
[ai]
239-
provider = "longcat" # Options: longcat, deepseek, openai, gemini, groq
239+
provider = "longcat" # Options: longcat, iflow, deepseek, openai, gemini, groq
240240
maxTokens = 8000
241241
temperature = 0.7
242242

config/steve-common.toml.example

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# AI Provider Configuration
22
[ai]
3-
# AI provider to use: 'longcat', 'deepseek', 'openai', 'gemini', or 'groq'
3+
# AI provider to use: 'longcat', 'iflow', 'deepseek', 'openai', 'gemini', or 'groq'
44
provider = "longcat"
55

66
# Maximum tokens per API request (applies to all LLM providers)
@@ -17,6 +17,14 @@
1717

1818
# LongCat model to use (LongCat-Flash-Chat, LongCat-Flash-Thinking, LongCat-Flash-Thinking-2601)
1919
model = "LongCat-Flash-Thinking-2601"
20+
21+
# iFlow API Configuration
22+
[iflow]
23+
# Your iFlow API key (get from: https://platform.iflow.cn/profile?tab=apiKey)
24+
apiKey = "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
25+
26+
# iFlow model to use (glm-4.6, kimi-k2, qwen3-coder-plus, qwen3-max, qwen3-vl-plus, deepseek-v3.2, tstars2.0, iflow-rome-30ba3b)
27+
model = "glm-4.6"
2028

2129
# DeepSeek API Configuration
2230
[deepseek]

src/main/java/com/steve/ai/config/SteveConfig.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ public class SteveConfig {
1414
public static final ForgeConfigSpec.ConfigValue<String> LONGCAT_API_KEY;
1515
public static final ForgeConfigSpec.ConfigValue<String> LONGCAT_MODEL;
1616

17+
// iFlow
18+
public static final ForgeConfigSpec.ConfigValue<String> IFLOW_API_KEY;
19+
public static final ForgeConfigSpec.ConfigValue<String> IFLOW_MODEL;
20+
1721
// DeepSeek
1822
public static final ForgeConfigSpec.ConfigValue<String> DEEPSEEK_API_KEY;
1923
public static final ForgeConfigSpec.ConfigValue<String> DEEPSEEK_MODEL;
@@ -41,7 +45,7 @@ public class SteveConfig {
4145
builder.comment("AI API Configuration").push("ai");
4246

4347
AI_PROVIDER = builder
44-
.comment("AI provider to use: 'longcat', 'deepseek', 'openai', 'gemini', or 'groq' (FASTEST, FREE)")
48+
.comment("AI provider to use: 'longcat', 'iflow', 'deepseek', 'openai', 'gemini', or 'groq' (FASTEST, FREE)")
4549
.define("provider", "longcat");
4650

4751
MAX_TOKENS = builder
@@ -66,6 +70,19 @@ public class SteveConfig {
6670

6771
builder.pop();
6872

73+
74+
builder.comment("iFlow API Configuration").push("iflow");
75+
76+
IFLOW_API_KEY = builder
77+
.comment("Your iFlow API key (get from: https://platform.iflow.cn/profile?tab=apiKey)")
78+
.define("apiKey", "");
79+
80+
IFLOW_MODEL = builder
81+
.comment("iFlow model to use (glm-4.6, kimi-k2, qwen3-coder-plus, qwen3-max, qwen3-vl-plus, deepseek-v3.2, tstars2.0, iflow-rome-30ba3b)")
82+
.define("model", "glm-4.6");
83+
84+
builder.pop();
85+
6986
builder.comment("DeepSeek API Configuration").push("deepseek");
7087

7188
DEEPSEEK_API_KEY = builder
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package com.steve.ai.llm;
2+
3+
import com.google.gson.JsonArray;
4+
import com.google.gson.JsonObject;
5+
import com.google.gson.JsonParser;
6+
import com.steve.ai.SteveMod;
7+
import com.steve.ai.config.SteveConfig;
8+
9+
import java.net.URI;
10+
import java.net.http.HttpClient;
11+
import java.net.http.HttpRequest;
12+
import java.net.http.HttpResponse;
13+
import java.time.Duration;
14+
15+
/**
16+
* Client for iFlow API - OpenAI-compatible
17+
* Endpoint: https://apis.iflow.cn/v1/chat/completions
18+
* API Keys: https://platform.iflow.cn/profile?tab=apiKey
19+
*/
20+
public class IFlowClient {
21+
private static final String IFLOW_API_URL = "https://apis.iflow.cn/v1/chat/completions";
22+
private static final int MAX_RETRIES = 3;
23+
private static final int INITIAL_RETRY_DELAY_MS = 1000;
24+
25+
private final HttpClient client;
26+
private final String apiKey;
27+
28+
public IFlowClient() {
29+
this.apiKey = SteveConfig.IFLOW_API_KEY.get();
30+
this.client = HttpClient.newBuilder()
31+
.connectTimeout(Duration.ofSeconds(30))
32+
.build();
33+
}
34+
35+
public String sendRequest(String systemPrompt, String userPrompt) {
36+
if (apiKey == null || apiKey.isEmpty()) {
37+
SteveMod.LOGGER.error("iFlow API key not configured!");
38+
return null;
39+
}
40+
41+
JsonObject requestBody = buildRequestBody(systemPrompt, userPrompt);
42+
43+
HttpRequest request = HttpRequest.newBuilder()
44+
.uri(URI.create(IFLOW_API_URL))
45+
.header("Authorization", "Bearer " + apiKey)
46+
.header("Content-Type", "application/json")
47+
.timeout(Duration.ofSeconds(60))
48+
.POST(HttpRequest.BodyPublishers.ofString(requestBody.toString()))
49+
.build();
50+
51+
for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
52+
try {
53+
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
54+
55+
if (response.statusCode() == 200) {
56+
return parseResponse(response.body());
57+
}
58+
59+
if (response.statusCode() == 429 || response.statusCode() >= 500) {
60+
if (attempt < MAX_RETRIES - 1) {
61+
long delayMs = (long) INITIAL_RETRY_DELAY_MS * (int) Math.pow(2, attempt);
62+
SteveMod.LOGGER.warn("iFlow API failed ({}), retrying in {}ms", response.statusCode(), delayMs);
63+
try {
64+
Thread.sleep(delayMs);
65+
} catch (InterruptedException ie) {
66+
Thread.currentThread().interrupt();
67+
return null;
68+
}
69+
continue;
70+
}
71+
}
72+
73+
SteveMod.LOGGER.error("iFlow API request failed: {}", response.statusCode());
74+
return null;
75+
} catch (Exception e) {
76+
if (attempt < MAX_RETRIES - 1) {
77+
long delayMs = (long) INITIAL_RETRY_DELAY_MS * (int) Math.pow(2, attempt);
78+
SteveMod.LOGGER.warn("Error communicating with iFlow API, retrying in {}ms", delayMs, e);
79+
try {
80+
Thread.sleep(delayMs);
81+
} catch (InterruptedException ie) {
82+
Thread.currentThread().interrupt();
83+
return null;
84+
}
85+
} else {
86+
SteveMod.LOGGER.error("Error communicating with iFlow API after {} attempts", MAX_RETRIES, e);
87+
return null;
88+
}
89+
}
90+
}
91+
return null;
92+
}
93+
94+
private JsonObject buildRequestBody(String systemPrompt, String userPrompt) {
95+
JsonObject body = new JsonObject();
96+
body.addProperty("model", SteveConfig.IFLOW_MODEL.get());
97+
body.addProperty("temperature", SteveConfig.TEMPERATURE.get());
98+
body.addProperty("max_tokens", SteveConfig.MAX_TOKENS.get());
99+
100+
JsonArray messages = new JsonArray();
101+
JsonObject systemMessage = new JsonObject();
102+
systemMessage.addProperty("role", "system");
103+
systemMessage.addProperty("content", systemPrompt);
104+
messages.add(systemMessage);
105+
106+
JsonObject userMessage = new JsonObject();
107+
userMessage.addProperty("role", "user");
108+
userMessage.addProperty("content", userPrompt);
109+
messages.add(userMessage);
110+
111+
body.add("messages", messages);
112+
return body;
113+
}
114+
115+
private String parseResponse(String responseBody) {
116+
try {
117+
JsonObject json = JsonParser.parseString(responseBody).getAsJsonObject();
118+
return json.getAsJsonArray("choices").get(0).getAsJsonObject()
119+
.getAsJsonObject("message").get("content").getAsString();
120+
} catch (Exception e) {
121+
SteveMod.LOGGER.error("Error parsing iFlow response", e);
122+
return null;
123+
}
124+
}
125+
}

src/main/java/com/steve/ai/llm/TaskPlanner.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
public class TaskPlanner {
1717
// Legacy synchronous clients (for backward compatibility)
1818
private final LongCatClient longCatClient;
19+
private final IFlowClient iflowClient;
1920
private final DeepSeekClient deepSeekClient;
2021
private final OpenAIClient openAIClient;
2122
private final GeminiClient geminiClient;
2223
private final GroqClient groqClient;
2324

2425
// NEW: Async resilient clients
2526
private final AsyncLLMClient asyncLongCatClient;
27+
private final AsyncLLMClient asyncIFlowClient;
2628
private final AsyncLLMClient asyncDeepSeekClient;
2729
private final AsyncLLMClient asyncOpenAIClient;
2830
private final AsyncLLMClient asyncGeminiClient;
@@ -33,6 +35,7 @@ public class TaskPlanner {
3335
public TaskPlanner() {
3436
// Legacy clients (always initialize - these work without external dependencies)
3537
this.longCatClient = new LongCatClient();
38+
this.iflowClient = new IFlowClient();
3639
this.deepSeekClient = new DeepSeekClient();
3740
this.openAIClient = new OpenAIClient();
3841
this.geminiClient = new GeminiClient();
@@ -42,6 +45,7 @@ public TaskPlanner() {
4245
LLMCache tempCache = null;
4346
LLMFallbackHandler tempFallback = null;
4447
AsyncLLMClient tempAsyncLongCat = null;
48+
AsyncLLMClient tempAsyncIFlow = null;
4549
AsyncLLMClient tempAsyncDeepSeek = null;
4650
AsyncLLMClient tempAsyncOpenAI = null;
4751
AsyncLLMClient tempAsyncGemini = null;
@@ -63,6 +67,13 @@ public TaskPlanner() {
6367
temperature
6468
);
6569

70+
AsyncLLMClient baseIFlow = new AsyncIFlowClient(
71+
SteveConfig.IFLOW_API_KEY.get(),
72+
SteveConfig.IFLOW_MODEL.get(),
73+
maxTokens,
74+
temperature
75+
);
76+
6677
AsyncLLMClient baseDeepSeek = new AsyncDeepSeekClient(
6778
SteveConfig.DEEPSEEK_API_KEY.get(),
6879
SteveConfig.DEEPSEEK_MODEL.get(),
@@ -93,6 +104,7 @@ public TaskPlanner() {
93104

94105
// Wrap with resilience patterns (caching, retries, circuit breaker)
95106
tempAsyncLongCat = new ResilientLLMClient(baseLongCat, tempCache, tempFallback);
107+
tempAsyncIFlow = new ResilientLLMClient(baseIFlow, tempCache, tempFallback);
96108
tempAsyncDeepSeek = new ResilientLLMClient(baseDeepSeek, tempCache, tempFallback);
97109
tempAsyncOpenAI = new ResilientLLMClient(baseOpenAI, tempCache, tempFallback);
98110
tempAsyncGemini = new ResilientLLMClient(baseGemini, tempCache, tempFallback);
@@ -106,6 +118,7 @@ public TaskPlanner() {
106118
this.llmCache = tempCache;
107119
this.fallbackHandler = tempFallback;
108120
this.asyncLongCatClient = tempAsyncLongCat;
121+
this.asyncIFlowClient = tempAsyncIFlow;
109122
this.asyncDeepSeekClient = tempAsyncDeepSeek;
110123
this.asyncOpenAIClient = tempAsyncOpenAI;
111124
this.asyncGeminiClient = tempAsyncGemini;
@@ -146,6 +159,7 @@ public ResponseParser.ParsedResponse planTasks(SteveEntity steve, String command
146159
private String getAIResponse(String provider, String systemPrompt, String userPrompt) {
147160
String response = switch (provider) {
148161
case "longcat" -> longCatClient.sendRequest(systemPrompt, userPrompt);
162+
case "iflow" -> iflowClient.sendRequest(systemPrompt, userPrompt);
149163
case "deepseek" -> deepSeekClient.sendRequest(systemPrompt, userPrompt);
150164
case "openai" -> openAIClient.sendRequest(systemPrompt, userPrompt);
151165
case "gemini" -> geminiClient.sendRequest(systemPrompt, userPrompt);
@@ -193,6 +207,7 @@ public CompletableFuture<ResponseParser.ParsedResponse> planTasksAsync(SteveEnti
193207
// Build params map with provider-specific model
194208
String modelForProvider = switch (provider) {
195209
case "longcat" -> SteveConfig.LONGCAT_MODEL.get();
210+
case "iflow" -> SteveConfig.IFLOW_MODEL.get();
196211
case "deepseek" -> SteveConfig.DEEPSEEK_MODEL.get();
197212
case "openai" -> SteveConfig.OPENAI_MODEL.get();
198213
case "gemini" -> SteveConfig.GEMINI_MODEL.get();
@@ -276,6 +291,7 @@ public CompletableFuture<ResponseParser.ParsedResponse> planTasksAsync(SteveEnti
276291
private AsyncLLMClient getAsyncClient(String provider) {
277292
AsyncLLMClient client = switch (provider) {
278293
case "longcat" -> asyncLongCatClient;
294+
case "iflow" -> asyncIFlowClient;
279295
case "deepseek" -> asyncDeepSeekClient;
280296
case "openai" -> asyncOpenAIClient;
281297
case "gemini" -> asyncGeminiClient;
@@ -289,8 +305,9 @@ private AsyncLLMClient getAsyncClient(String provider) {
289305
// Null check - if preferred client is null, try fallback options
290306
if (client == null) {
291307
SteveMod.LOGGER.warn("[Async] Client for provider '{}' is null, trying fallbacks", provider);
292-
// Try fallback order: longcat -> deepseek -> openai -> gemini -> groq
308+
// Try fallback order: longcat -> iflow -> deepseek -> openai -> gemini -> groq
293309
if (asyncLongCatClient != null) return asyncLongCatClient;
310+
if (asyncIFlowClient != null) return asyncIFlowClient;
294311
if (asyncDeepSeekClient != null) return asyncDeepSeekClient;
295312
if (asyncOpenAIClient != null) return asyncOpenAIClient;
296313
if (asyncGeminiClient != null) return asyncGeminiClient;

0 commit comments

Comments
 (0)