XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/gpt4free

Flush changes

b741a0bf
hlohaus <983577+hlohaus@users.noreply.github.com>
提交于

代码差异

12 个文件 +244 -44
Modified g4f-go/download.go +42 -17
@@ -381,27 +381,52 @@ func IsTermuxSystem() bool {
381 381 return !os.IsNotExist(err)
382 382 }
383 383
384 // ensureTermuxRuntime installs python and build dependencies via Termux's
385 // package manager and returns the system python path. No CPython download is
386 // needed — Termux provides a native python package.
387 func ensureTermuxRuntime() (string, error) {
388 // Packages required for g4f and native extensions.
389 required := []string{"python", "clang", "make", "libxml2", "libxslt", "libjpeg-turbo", "libpng"}
390 var missing []string
391 for _, pkg := range required {
392 if !termuxPackageInstalled(pkg) {
393 missing = append(missing, pkg)
394 }
395 }
396 if len(missing) > 0 {
397 fmt.Printf("Installing Termux packages: %s\n", strings.Join(missing, ", "))
398 args := append([]string{"install", "-y"}, missing...)
399 cmd := exec.Command("pkg", args...)
400 cmd.Stdout = os.Stdout
401 cmd.Stderr = os.Stderr
402 if err := cmd.Run(); err != nil {
403 return "", fmt.Errorf("pkg install failed: %w", err)
404 }
405 }
406
407 py, err := exec.LookPath("python3")
408 if err != nil {
409 py, err = exec.LookPath("python")
410 }
411 if err != nil {
412 return "", fmt.Errorf("python not found in Termux PATH (run 'pkg install python')")
413 }
414 return py, nil
415 }
416
417 // termuxPackageInstalled checks if a Termux package is already installed
418 // using dpkg.
419 func termuxPackageInstalled(pkg string) bool {
420 cmd := exec.Command("dpkg", "-s", pkg)
421 return cmd.Run() == nil
422 }
423
384 424 // ensureRuntime is the top-level entry point. It downloads (if needed) and
385 425 // extracts the platform runtime into binDir, then returns the python
386 426 // executable/launcher path.
387 427 func ensureRuntime() (string, error) {
388 isTermux := IsTermuxSystem()
389 if isTermux {
390 // Check if key compilers/tools are installed
391 if !commandExists("clang") || !commandExists("make") {
392 fmt.Println("Missing required tools. Installing...")
393
394 packages := []string{"clang", "make", "libxml2", "libxslt", "libjpeg-turbo", "libpng"}
395 args := append([]string{"install", "-y"}, packages...)
396
397 cmd := exec.Command("pkg", args...)
398 cmd.Stdout = os.Stdout
399 cmd.Stderr = os.Stderr
400
401 if err := cmd.Run(); err != nil {
402 fmt.Printf("Error during package installation: %v\n", err)
403 }
404 }
428 if IsTermuxSystem() {
429 return ensureTermuxRuntime()
405 430 }
406 431
407 432 binDir := installDir()
Modified g4f-go/main.go +22 -3
@@ -108,9 +108,18 @@ func runMain() int {
108 108 fmt.Fprintln(os.Stderr, "g4f-go:", err)
109 109 return 1
110 110 }
111 start := time.Now()
112 if err := installG4F(binDir, exe, start); err != nil {
113 fmt.Fprintln(os.Stderr, "g4f-go:", err)
111 if !g4fIsInstalled(binDir) {
112 // First call: install g4f.
113 start := time.Now()
114 if err := installG4F(binDir, exe, start); err != nil {
115 fmt.Fprintln(os.Stderr, "g4f-go:", err)
116 }
117 } else if len(args) > 0 && isUpgradeCommand(args[0]) {
118 // Subsequent call with gui/api/dev: upgrade g4f.
119 start := time.Now()
120 if err := upgradeG4F(binDir, exe, start); err != nil {
121 fmt.Fprintln(os.Stderr, "g4f-go:", err)
122 }
114 123 }
115 124
116 125 // Default: forward everything to the g4f module.
@@ -130,6 +139,16 @@ func normalizeArgs(args []string) []string {
130 139 return args
131 140 }
132 141
142 // isUpgradeCommand reports whether the subcommand should trigger an
143 // automatic g4f upgrade (only when g4f is already installed).
144 func isUpgradeCommand(arg string) bool {
145 switch arg {
146 case "api", "gui", "dev":
147 return true
148 }
149 return false
150 }
151
133 152 // hasSubcommand reports whether args start with a known g4f-go subcommand.
134 153 func hasSubcommand(args []string) bool {
135 154 if len(args) == 0 {
Modified g4f-go/runtime.go +27 -0
@@ -108,6 +108,14 @@ func extractEmbedded(binDir string) error {
108 108 return nil
109 109 }
110 110
111 // g4fIsInstalled reports whether the g4f Python package is already installed
112 // in the downloaded runtime by checking the .installed stamp.
113 func g4fIsInstalled(binDir string) bool {
114 stamp := filepath.Join(binDir, ".g4f-runtime", ".installed")
115 _, err := os.Stat(stamp)
116 return err == nil
117 }
118
111 119 // installG4F installs the g4f Python package into the downloaded runtime and
112 120 // writes the .installed stamp. Uses the bundled pip (ensurepip wheels in
113 121 // pbs installs, bootstrapped via `python -m ensurepip`) so no network access
@@ -135,6 +143,25 @@ func installG4F(binDir, exe string, start time.Time) error {
135 143 return nil
136 144 }
137 145
146 // upgradeG4F upgrades the g4f Python package in the downloaded runtime.
147 // Called on subsequent runs (after the initial install) when the user passes
148 // a subcommand that benefits from the latest version (api, gui, dev).
149 func upgradeG4F(binDir, exe string, start time.Time) error {
150 fmt.Printf("Upgrading gpt4free...\n")
151 if err := ensurePip(binDir, exe); err != nil {
152 return err
153 }
154 code, err := runPython(noSignalCtx(), exe, []string{
155 "-m", "pip", "install",
156 "--no-input", "--upgrade", "g4f[slim]",
157 }, pipEnv(binDir)...)
158 if err != nil || code != 0 {
159 return fmt.Errorf("pip upgrade g4f failed (exit %d): %w", code, err)
160 }
161 fmt.Printf("gpt4free upgraded in %.1fs\n", time.Since(start).Seconds())
162 return nil
163 }
164
138 165 // ensurePip makes `python -m pip` available in the downloaded runtime.
139 166 // pbs installs ship ensurepip but no standalone pip; bootstrap once.
140 167 func ensurePip(binDir, exe string) error {
Modified g4f/Provider/DeepInfra.py +1 -1
@@ -16,7 +16,7 @@ def _get_turnstile_token_sync(model: str) -> str:
16 16 """
17 17 import time
18 18
19 for attempt in range(3):
19 for attempt in range(1):
20 20 session = SyncCDPSession(headless=False)
21 21 session.start_chrome()
22 22
Modified g4f/Provider/audio/OpenAIFM.py +1 -1
@@ -46,7 +46,7 @@ class OpenAIFM(AsyncGeneratorProvider, ProviderModelMixin):
46 46 models = styles + voices
47 47
48 48 @classmethod
49 def get_grouped_models(cls):
49 def get_grouped_models(cls, **kwargs):
50 50 return [
51 51 {"group": "Styles", "models": cls.styles},
52 52 {"group": "Voices", "models": cls.voices},
Modified g4f/Provider/local/Ollama.py +1 -3
@@ -8,13 +8,11 @@ from typing import Optional
8 8
9 9 from ..template import OpenaiTemplate
10 10 from ...requests import StreamSession, raise_for_status
11 from ...providers.response import Usage, Reasoning
12 11 from ...cookies import get_cookies
13 12 from ...tools.run_tools import AuthManager
14 13 from ...typing import AsyncResult, Messages
15 14 from ...config import AppConfig
16 15 from ...errors import MissingAuthError
17 from ... import debug
18 16
19 17
20 18 class Ollama(OpenaiTemplate):
@@ -27,7 +25,7 @@ class Ollama(OpenaiTemplate):
27 25 active_by_default = True
28 26 local_models: list[str] = []
29 27 model_aliases = {"gpt-oss-120b": "gpt-oss:120b", "gpt-oss-20b": "gpt-oss:20b"}
30 default_model = "nemotron-3-super"
28 default_model = "nemotron-3-nano:30b"
31 29
32 30 @classmethod
33 31 async def get_quota(cls, api_key: Optional[str] = None) -> Optional[dict]:
Modified g4f/Provider/needs_auth/Airforce.py +1 -1
@@ -13,7 +13,7 @@ class Airforce(OpenaiTemplate):
13 13 working = True
14 14 active_by_default = True
15 15 use_image_size = True
16 default_model = "gpt-4o-mini"
16 default_model = "unmoderated-gpt"
17 17
18 18 @classmethod
19 19 async def create_async_generator(
Modified g4f/Provider/needs_auth/Nvidia.py +1 -2
@@ -1,7 +1,6 @@
1 1 from __future__ import annotations
2 2
3 3 from ..template import OpenaiTemplate
4 from ...config import DEFAULT_MODEL
5 4
6 5
7 6 class Nvidia(OpenaiTemplate):
@@ -12,5 +11,5 @@ class Nvidia(OpenaiTemplate):
12 11 url = "https://build.nvidia.com"
13 12 working = True
14 13 active_by_default = True
15 default_model = DEFAULT_MODEL
14 default_model = "nvidia/nemotron-3.5-lightning-30b-a3b"
16 15 add_user = False
Modified g4f/requests/raise_for_status.py +14 -16
@@ -40,7 +40,6 @@ async def raise_for_status_async(
40 40 ):
41 41 if response.ok:
42 42 return
43 is_html = False
44 43 if message is None:
45 44 content_type = response.headers.get("content-type", "")
46 45 if content_type.startswith("application/json"):
@@ -58,13 +57,13 @@ async def raise_for_status_async(
58 57 except json.JSONDecodeError:
59 58 message = await response.text()
60 59 else:
61 message = (await response.text()).strip()
62 is_html = content_type.startswith(
60 message = await response.text()
61 if content_type.startswith(
63 62 "text/html"
64 ) or message.lower().startswith("<!DOCTYPE".lower())
65 if message is None or is_html:
66 if response.status == 520:
67 message = "Unknown error (Cloudflare)"
63 ) or message.strip().lower().startswith("<!DOCTYPE".lower()):
64 message = "HTML content"
65 if response.status == 520:
66 message = "Unknown error (Cloudflare)"
68 67 if response.status in (429, 402):
69 68 raise RateLimitError(f"Response {response.status}: {message}")
70 69 if response.status == 401:
@@ -81,7 +80,7 @@ async def raise_for_status_async(
81 80 raise MissingAuthError(f"Response {response.status}: Invalid API key")
82 81 else:
83 82 raise ResponseStatusError(
84 f"Response {response.status}: {'HTML content' if is_html else message}"
83 f"Response {response.status}: {message}"
85 84 )
86 85
87 86
@@ -93,15 +92,14 @@ def raise_for_status(
93 92 return raise_for_status_async(response, message)
94 93 if response.ok:
95 94 return
96 is_html = False
97 if message is None:
98 is_html = response.headers.get("content-type", "").startswith(
95 if response.headers.get("content-type", "").startswith(
99 96 "text/html"
100 ) or response.text.startswith("<!DOCTYPE")
97 ) or response.text.strip().lower().startswith("<!DOCTYPE".lower()):
98 message = "HTML content"
99 elif message is None:
101 100 message = response.text
102 if message is None or is_html:
103 if response.status_code == 520:
104 message = "Unknown error (Cloudflare)"
101 if response.status_code == 520:
102 message = "Unknown error (Cloudflare)"
105 103 if response.status_code in (429, 402):
106 104 raise RateLimitError(f"Response {response.status_code}: {message}")
107 105 if response.status_code == 401:
@@ -118,5 +116,5 @@ def raise_for_status(
118 116 raise MissingAuthError(f"Response {response.status_code}: Invalid API key")
119 117 else:
120 118 raise ResponseStatusError(
121 f"Response {response.status_code}: {'HTML content' if is_html else message}"
119 f"Response {response.status_code}: {message}"
122 120 )
Modified projects/discord-bot/.env.example +40 -0
@@ -32,3 +32,43 @@ G4F_MAX_HISTORY=12
32 32 # are silently blocked. Useful for announcement or trap channels where
33 33 # you don't want command spam.
34 34 # G4F_HONEYPOT_CHANNELS=111111111111111111
35
36 # ---------------------------------------------------------------------------
37 # Live feed
38 # ---------------------------------------------------------------------------
39 # When G4F_LIVE_FEED_CHANNEL is set, the bot posts a live activity feed
40 # to that Discord channel:
41 # - 🖼️ thumbnails of generated images
42 # - 🔧 tool calls (web search, scraping, image gen, ...)
43 # - 📝 file edits (apply_patch, file_write, file_delete)
44 # - ⚡ heavy token usage completions
45 # - 🚨 server errors (5xx)
46 # - 👋 new g4f.dev users
47 # - 📊 periodic activity summaries
48 #
49 # Leave unset to disable the feed.
50 # G4F_LIVE_FEED_CHANNEL=123456789012345678
51
52 # Base URL of the g4f API server (where /api/logs is served).
53 # G4F_API_BASE=http://localhost:8080
54
55 # Public base URL for Discord-accessible image/thumbnail links.
56 # Defaults to G4F_API_BASE. Set this if the API server is behind a tunnel
57 # or reverse proxy and Discord needs a different host to fetch images.
58 # G4F_PUBLIC_BASE=https://your-public-host.example
59
60 # g4f.dev base URL for new-user announcements. Set to empty to disable.
61 # G4F_MEMBERS_BASE=https://g4f.dev
62
63 # Seconds between feed polls (default: 15).
64 # G4F_FEED_POLL_INTERVAL=15
65
66 # Token count that flags a completion as "heavy" (default: 10000).
67 # G4F_HEAVY_TOKEN_THRESHOLD=10000
68
69 # Seconds between activity summaries (default: 3600 = 1 hour).
70 # G4F_FEED_SUMMARY_INTERVAL=3600
71
72 # Max embeds posted per poll cycle (default: 5). Prevents spam if many
73 # events arrive at once.
74 # G4F_FEED_MAX_POSTS_PER_CYCLE=5
Modified projects/discord-bot/README.md +55 -0
@@ -12,6 +12,7 @@ A Discord bot powered by [gpt4free (g4f)](https://github.com/xtekky/gpt4free) th
12 12 - 🛠️ **MCP tool-calling** — the AI can autonomously call tools (web search, web scraping, image generation, text-to-audio, and more) via g4f's built-in MCP server. The bot executes the tool, feeds the result back, and loops until the AI has a final answer.
13 13 - ⚡ **Streaming responses** — edits the message in-place for a live "typing" effect
14 14 - 🔒 Per-user history isolation with configurable length
15 - 📡 **Live activity feed** — an optional channel that mirrors g4f activity in real time: image thumbnails, tool calls, file edits, heavy token usage, server errors, new g4f.dev users, and periodic summaries.
15 16
16 17 ## Setup
17 18
@@ -134,11 +135,65 @@ See all available providers with:
134 135 g4f --help
135 136 ```
136 137
138 ## Live activity feed
139
140 The bot can mirror g4f activity into a dedicated Discord channel in real time. Enable it by setting `G4F_LIVE_FEED_CHANNEL` to a channel ID in your `.env`.
141
142 ### What gets posted
143
144 | Event | Trigger | Example |
145 |---|---|---|
146 | 🖼️ Image Generated | Any `/v1/images/generate` or `/v1/media/generate` request | Embed with the thumbnail + full-size link |
147 | 🔧 Tool Calls | A chat completion whose response includes `tool_calls` | Lists tool names, model, prompt snippet |
148 | 📝 File Edit | A tool call to `apply_patch`, `file_write`, or `file_delete` | Highlighted separately from other tools |
149 | ⚡ Heavy Token Usage | A completion using ≥ `G4F_HEAVY_TOKEN_THRESHOLD` tokens | Shows prompt/completion/total token counts |
150 | 🚨 Server Error | Any request returning a `5xx` status | Path, status, duration |
151 | 👋 New g4f.dev User | A new user appears in `/members/api/recent-users` | Username, provider, tier, avatar |
152 | 📊 Activity Summary | Every `G4F_FEED_SUMMARY_INTERVAL` seconds | Rolling counts + top models/providers |
153
154 ### How it works
155
156 The `LiveFeed` cog (in `live_feed.py`) polls two sources on a configurable interval (default 15 s):
157
158 1. **`{G4F_API_BASE}/api/logs`** — the g4f API server's request log. The cog remembers the last seen log id and only processes new entries. Image URLs pointing at `/media/` or `/images/` are rewritten to `/thumbnail/` (using `G4F_PUBLIC_BASE`) so Discord can fetch compact previews.
159 2. **`{G4F_MEMBERS_BASE}/members/api/recent-users`** — a public endpoint on the g4f.dev members worker that returns the most recently created users. The cog tracks seen `provider:username` keys and announces new ones.
160
161 To keep the channel readable, at most `G4F_FEED_MAX_POSTS_PER_CYCLE` embeds are posted per poll cycle (additional events are still counted toward the periodic summary).
162
163 ### Setup
164
165 1. Create a dedicated channel in your Discord server (e.g. `#g4f-live`).
166 2. Copy its channel ID (right-click → Copy ID, with Developer Mode enabled).
167 3. Add to `.env`:
168
169 ```bash
170 G4F_LIVE_FEED_CHANNEL=123456789012345678
171 G4F_API_BASE=http://localhost:8080 # where the g4f API runs
172 G4F_PUBLIC_BASE=https://your-public-host # optional, for Discord-accessible image links
173 G4F_MEMBERS_BASE=https://g4f.dev # set empty to disable new-user posts
174 ```
175
176 4. Restart the bot. You should see `Live feed cog loaded → channel ...` in the logs.
177
178 ### Configuration reference
179
180 | Variable | Default | Description |
181 |---|---|---|
182 | `G4F_LIVE_FEED_CHANNEL` | *(unset)* | Discord channel ID for the feed. Unset = disabled. |
183 | `G4F_API_BASE` | `http://localhost:8080` | g4f API base URL (must expose `/api/logs`). |
184 | `G4F_PUBLIC_BASE` | = `G4F_API_BASE` | Public base URL for Discord-accessible image/thumbnail links. |
185 | `G4F_MEMBERS_BASE` | `https://g4f.dev` | g4f.dev base URL for new-user posts. Empty = disabled. |
186 | `G4F_FEED_POLL_INTERVAL` | `15` | Seconds between polls. |
187 | `G4F_HEAVY_TOKEN_THRESHOLD` | `10000` | Token count that flags a completion as "heavy". |
188 | `G4F_FEED_SUMMARY_INTERVAL` | `3600` | Seconds between activity summaries. |
189 | `G4F_FEED_MAX_POSTS_PER_CYCLE` | `5` | Max embeds per poll cycle (anti-spam). |
190
137 191 ## Project structure
138 192
139 193 ```
140 194 projects/discord-bot/
141 195 ├── bot.py # Main bot logic (commands, tool-calling loop)
196 ├── live_feed.py # Live activity feed cog (image/tool/token/new-user events)
142 197 ├── mcp_tools.py # MCP tool manager (definitions, execution, display)
143 198 ├── .env.example # Template environment file
144 199 └── README.md # This file
Modified projects/discord-bot/bot.py +39 -0
@@ -32,6 +32,7 @@ from g4f.providers.any_provider import AnyProvider
32 32 from g4f.client import ClientFactory
33 33
34 34 from mcp_tools import MCPToolManager, ALL_AVAILABLE_TOOLS, SAFE_DEFAULT_TOOLS
35 from live_feed import LiveFeed
35 36
36 37 load_dotenv()
37 38
@@ -52,6 +53,22 @@ MAX_HISTORY = int(os.getenv("G4F_MAX_HISTORY", "12")) # messages per user
52 53 PROXY = os.getenv("G4F_PROXY") # optional, e.g. "socks5://127.0.0.1:1080"
53 54 MAX_TOOL_LOOPS = int(os.getenv("G4F_MAX_TOOL_LOOPS", "4")) # safety cap
54 55
56 # ---------------------------------------------------------------------------
57 # Live feed configuration
58 # ---------------------------------------------------------------------------
59 # When set, the bot posts a live activity feed to this Discord channel:
60 # image thumbnails, tool calls, file edits, heavy token usage, server
61 # errors, new g4f.dev users, and periodic summaries.
62 LIVE_FEED_CHANNEL = int(os.getenv("G4F_LIVE_FEED_CHANNEL", "0") or "0")
63 API_BASE = os.getenv("G4F_API_BASE", "http://localhost:8080")
64 PUBLIC_BASE = os.getenv("G4F_PUBLIC_BASE", API_BASE)
65 PUBLIC_API_KEY = os.getenv("G4F_PUBLIC_API_KEY", "")
66 MEMBERS_BASE = os.getenv("G4F_MEMBERS_BASE", "https://auth.g4f.dev")
67 FEED_POLL_INTERVAL = int(os.getenv("G4F_FEED_POLL_INTERVAL", "15"))
68 HEAVY_TOKEN_THRESHOLD = int(os.getenv("G4F_HEAVY_TOKEN_THRESHOLD", "10000"))
69 FEED_SUMMARY_INTERVAL = int(os.getenv("G4F_FEED_SUMMARY_INTERVAL", "3600"))
70 FEED_MAX_POSTS_PER_CYCLE = int(os.getenv("G4F_FEED_MAX_POSTS_PER_CYCLE", "5"))
71
55 72 # Simple validation for runtime model names.
56 73 ALLOWED_MODEL_CHARS = set(
57 74 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.:"
@@ -741,6 +758,28 @@ async def on_ready():
741 758 except Exception:
742 759 log.exception("Failed to sync slash commands")
743 760
761 # Load the live feed cog if a channel was configured.
762 if LIVE_FEED_CHANNEL:
763 existing = bot.get_cog("LiveFeed")
764 if existing is None:
765 await bot.add_cog(
766 LiveFeed(
767 bot=bot,
768 channel_id=LIVE_FEED_CHANNEL,
769 api_base=API_BASE,
770 public_base=PUBLIC_BASE,
771 api_key=PUBLIC_API_KEY,
772 members_base=MEMBERS_BASE or None,
773 poll_interval=FEED_POLL_INTERVAL,
774 heavy_token_threshold=HEAVY_TOKEN_THRESHOLD,
775 summary_interval=FEED_SUMMARY_INTERVAL,
776 max_posts_per_cycle=FEED_MAX_POSTS_PER_CYCLE,
777 )
778 )
779 log.info("Live feed cog loaded → channel %s", LIVE_FEED_CHANNEL)
780 else:
781 log.info("Live feed cog already loaded")
782
744 783
745 784 def main():
746 785 if not TOKEN: