返回提交历史
Modified
.gitignore
+1
-1
Modified
g4f-go/Makefile
+1
-2
Modified
g4f-go/README.md
+53
-69
Modified
g4f-go/build-all.sh
+9
-11
Added
g4f-go/build/g4f-go-0.1.0-android-arm64.zip
+0
-0
Added
g4f-go/build/g4f-go-0.1.0-linux-amd64.zip
+0
-0
Added
g4f-go/build/g4f-go-0.1.0-linux-arm64.zip
+0
-0
Added
g4f-go/download.go
+448
-0
Modified
g4f-go/fetch-python.sh
+55
-165
Modified
g4f-go/main.go
+24
-24
Added
g4f-go/manifest_embedded.go
+23
-0
Modified
g4f-go/process.go
+31
-18
Modified
g4f-go/runtime.go
+31
-51
Added
g4f-go/runtime.json
+50
-0
Added
g4f-go/runtime_android.go
+196
-0
Added
g4f-go/runtime_stubs.go
+18
-0
Modified
g4f/Provider/DeepInfra.py
+8
-5
Modified
g4f/Provider/template/OpenaiTemplate.py
+1
-1
XFEstudio/gpt4free
Improve build
cb51c35e
代码差异
18 个文件
+949
-347
@@ -16,4 +16,4 @@ g4f.exe
16
16
har_and_cookies
17
17
playground
18
18
.vscode
19
build
19
@@ -2,7 +2,7 @@
2
2
3
3
all: fetch build
4
4
5
# Download & merge the CPython embeddable runtimes (needs curl/unzip/zip/python3)
5
# Fetch the runtime manifest (validates URLs; runtimes download at first run)
6
6
fetch:
7
7
./fetch-python.sh
8
8
@@ -12,7 +12,6 @@ build:
12
12
13
13
clean:
14
14
rm -rf dist
15
find embed -name '*.zip' -delete
16
15
17
16
vet:
18
17
go vet ./...
@@ -1,8 +1,10 @@
1
1
# g4f-go
2
2
3
A single self-contained executable for [gpt4free](https://github.com/xtekky/gpt4free):
4
a small Go launcher with an embedded, relocatable CPython runtime that has
5
gpt4free pre-installed. No system Python required.
3
A small, self-contained Go launcher for [gpt4free](https://github.com/xtekky/gpt4free).
4
Unlike the original design, the CPython runtime is **not embedded** in the
5
binary: `g4f-go` is a few MB, and downloads the correct CPython for the host
6
platform on first run (with live progress feedback), then installs `g4f` into
7
it. No system Python required.
6
8
7
9
```
8
10
g4f-go client "What is gpt4free?"
@@ -11,57 +13,40 @@ g4f-go api --port 8080
11
13
12
14
## How it works
13
15
14
1. `fetch-python.sh` downloads the official CPython *embeddable* zips from
15
python.org for every supported platform, copies the `g4f` package into the
16
interpreter's `python-home/`, drops prebuilt wheels in `wheels/`, patches
17
`python311._pth` for relocation, and re-packs everything into
18
`embed/<os>/<os>-<arch>-embed-<ver>.zip`.
19
2. `go:embed` (build tags per OS) bakes that archive into the launcher binary.
20
3. On first run the launcher extracts the archive next to itself, finishes the
21
g4f install from the bundled wheels (offline), then execs
22
`python -m g4f <args>`.
23
4. `build-all.sh` cross-compiles the launcher with `CGO_ENABLED=0` for every
24
target and zips each release.
16
1. The binary embeds only a small manifest (`runtime.json`) that pins the
17
CPython archive URL, size, and sha256 per platform
18
(pbs install-only tarballs for desktop, the official python.org Android
19
package for Termux).
20
2. On first run `g4f-go` picks the entry for its host OS/arch, downloads the
21
archive into `~/.g4f/python-embed/` (or the app dir on Android), printing a
22
`\r` progress line (percent, bytes, throughput, ETA), verifies the sha256,
23
and extracts it.
24
3. It then bootstraps pip (`ensurepip`), installs `g4f[slim]` into the
25
interpreter, writes a `.installed` stamp, and finally runs your command.
26
4. Subsequent runs skip straight to step 3 — the runtime is cached until the
27
pinned version changes.
25
28
26
```
27
┌──────────────────────────────┐
28
│ g4f-go (Go, ~10 MB binary) │
29
│ ┌────────────────────────┐ │
30
│ │ CPython 3.14 runtime │ │ ← baked in via go:embed
31
│ │ + g4f + wheels │ │
32
│ └────────────────────────┘ │
33
└──────────────────────────────┘
34
│ first run: extract next to binary
35
▼
36
g4f-go.exe/ + python-home/ + wheels/ + .g4f-runtime/.installed
37
│
38
▼
39
exec python -m g4f <args>
40
```
41
42
## Build
29
Downloads go to:
43
30
44
Requires Go 1.22+ and (for `fetch-python.sh`) bash, curl, unzip, zip, python3.
31
| Platform | Location |
32
|---|---|
33
| Linux / macOS / Windows | `~/.g4f/python-embed/` |
34
| Android (Termux) | app-private dir (`G4F_ANDROID_FILES_DIR`, defaults to `$HOME/g4f-go-runtime`) |
45
35
46
```bash
47
cd g4f-go
36
`G4F_PYTHON_ONLY=1 g4f-go --version` prints the downloaded interpreter path
37
without running gpt4free (useful for wrapping the runtime from other tools).
48
38
49
# 1. Download & embed the Python runtimes (all platforms, ~200 MB on disk)
50
./fetch-python.sh
51
./fetch-python.sh linux # or just one platform
39
## Building
52
40
53
# 2. Build launchers for every OS/arch
54
./build-all.sh # -> dist/g4f-go-<ver>-<os>-<arch>.zip
55
# or a single one:
56
go build -o g4f-go .
41
```
42
go build -o g4f-go . # linux host build (fast iteration)
43
./build-all.sh # cross-compile + zip releases for all targets
44
./build-all.sh android # only the android target
45
./fetch-python.sh # optional: re-pin sizes + sha256 in runtime.json
57
46
```
58
47
59
Release zips live in `dist/`. Each zip is a portable folder: drop it anywhere,
60
run `g4f-go`, and the runtime is extracted next to it on first launch.
61
62
> **Size note:** CPython runtimes are ~40-60 MB per platform, so release zips
63
> are large (Windows ≈ 160 MB, Linux ≈ 820 MB). The `.zip` is stored inside the
64
> Go binary, and extracted on first run; the Go executable itself stays small.
48
The manifest is embedded via `go:embed runtime.json`; the binary builds
49
without network access.
65
50
66
51
## Usage
67
52
@@ -69,40 +54,39 @@ run `g4f-go`, and the runtime is extracted next to it on first launch.
69
54
g4f-go <g4f args...> run gpt4free (e.g. g4f-go client "hello")
70
55
g4f-go api --port 8080 start the OpenAI-compatible API server
71
56
g4f-go gui launch the web GUI
72
g4f-go status show embedded runtime status
57
g4f-go status show runtime download/install status
73
58
g4f-go install g4f (re)install the g4f package (network)
74
59
g4f-go help show help
75
60
```
76
61
77
Environment: `G4F_PYTHON_ONLY=1 g4f-go --version` prints the embedded python
78
path (useful for wrapping the runtime from other tools).
79
80
62
## Supported platforms
81
63
82
| OS | Arch | Notes |
83
|----|------|-------|
84
| Linux | amd64, arm64 | official python.org x64 embed |
85
| Windows | amd64, x86 | official python.org win64/win32 embed |
86
| macOS | arm64 (universal2 embed), amd64 | universal2 zip runs on both |
87
| FreeBSD | amd64 | reuses the Linux x64 embed (Python-only, no C extensions) |
64
| OS | Arch | Runtime source |
65
|----|------|----------------|
66
| Linux | amd64, arm64 | python-build-standalone install-only tarball |
67
| Windows | amd64 | python-build-standalone install-only tarball |
68
| macOS | amd64, arm64 | python-build-standalone install-only tarball |
69
| Android | arm64 (Termux) | official python.org `*-linux-android` package |
88
70
89
`fetch-python.sh` can be extended to other OSes; add a row and a matching
90
`runtime_<os>.go` build-tagged file.
71
Android note: the python.org Android package ships `libpython3.14.so` +
72
stdlib but no `python` executable. `g4f-go` detects Termux
73
(`pm list packages`), merges the tarball into the app dir, and compiles a tiny
74
C runner with Termux's clang that `dlopen`s libpython — the same technique as
75
CPython's own android testbed.
91
76
92
## Layout inside a release folder
77
## Layout after first run (`~/.g4f/python-embed/`)
93
78
94
79
```
95
g4f-go (binary)
96
python.exe / python (launcher)
97
python-home/ g4f package + site-packages
98
wheels/ prebuilt g4f dependency wheels
99
.g4f-runtime/.installed (stamp written on first run)
80
python-home/bin/python interpreter (pbs layout)
81
python-home/lib/python3.14 stdlib + site-packages (g4f installed here)
82
python (launcher) shell wrapper that sets PYTHONHOME/PYTHONPATH
83
.g4f-runtime/.runtime-ok stamp: download+extract complete
84
.g4f-runtime/.installed stamp: g4f pip-installed
100
85
```
101
86
102
87
## Limitations
103
88
104
- The runtime is extracted to disk next to the executable (this is what makes
105
it *relocatable* — CPython cannot run a 100%-in-memory interpreter reliably).
106
- Bundled dependency versions are pinned by `requirements-min.txt` at build
107
time; use `g4f-go install g4f` for network installs.
89
- The runtime is materialized on disk (CPython cannot run a 100%-in-memory
90
interpreter reliably); first run downloads ~50–800 MB depending on platform.
108
91
- macOS builds must be signed/notarized by the distributor for Gatekeeper.
92
- Android builds need Termux installed to compile the dlopen runner on device.
@@ -2,13 +2,13 @@
2
2
# build-all.sh
3
3
#
4
4
# Cross-compiles the g4f-go launcher for every supported OS/arch and packs a
5
# release zip per platform. The embedded Python archive is expected to have
6
# been prepared by ./fetch-python.sh first (the launcher still builds without
7
# it, but then refuses to run).
5
# release zip per platform. The runtime is NOT embedded: it downloads from
6
# python.org / python-build-standalone on first run (see runtime.json).
7
# `./fetch-python.sh` only pins sizes+shas; the launcher builds without it.
8
8
#
9
9
# Usage:
10
10
# ./build-all.sh # build every target
11
# ./build-all.sh linux # build only one OS (linux|windows|darwin|bsd)
11
# ./build-all.sh linux # build only one OS (linux|windows|darwin|android)
12
12
# G4F_VERSION=0.1.0 ./build-all.sh # custom version
13
13
14
14
set -euo pipefail
@@ -19,13 +19,12 @@ VERSION="${G4F_VERSION:-0.1.0}"
19
19
OUT="${OUT:-$HERE/dist}"
20
20
mkdir -p "$OUT"
21
21
22
# Optional OS filter: maps the fetch-python.sh platform names (linux,
23
# windows, darwin, bsd) onto GOOS. Empty = build everything.
22
# Optional OS filter: maps the fetch-python.sh platform names onto GOOS.
24
23
WANT_OS="${1:-${OS_ONLY:-}}"
25
24
case "$WANT_OS" in
26
"linux"|"windows"|"darwin"|"bsd") echo "==> Building only: $WANT_OS" ;;
25
"linux"|"windows"|"darwin"|"android") echo "==> Building only: $WANT_OS" ;;
27
26
"") echo "==> Building all targets" ;;
28
*) echo "Unknown OS filter: $WANT_OS (expected linux|windows|darwin|bsd)" >&2; exit 1 ;;
27
*) echo "Unknown OS filter: $WANT_OS (expected linux|windows|darwin|android)" >&2; exit 1 ;;
29
28
esac
30
29
31
30
# os arch ext name
@@ -33,10 +32,9 @@ TARGETS=(
33
32
"linux amd64 g4f-go"
34
33
"linux arm64 g4f-go"
35
34
"windows amd64 g4f-go.exe"
36
"windows 386 g4f-go.exe"
37
35
"darwin arm64 g4f-go"
38
36
"darwin amd64 g4f-go"
39
"freebsd amd64 g4f-go"
37
"android arm64 g4f-go"
40
38
)
41
39
42
40
for t in "${TARGETS[@]}"; do
@@ -45,7 +43,7 @@ for t in "${TARGETS[@]}"; do
45
43
"linux") [[ "$GOOS" == "linux" ]] || continue ;;
46
44
"windows") [[ "$GOOS" == "windows" ]] || continue ;;
47
45
"darwin") [[ "$GOOS" == "darwin" ]] || continue ;;
48
"bsd") [[ "$GOOS" == "freebsd" ]] || continue ;;
46
"android") [[ "$GOOS" == "android" ]] || continue ;;
49
47
"") ;;
50
48
esac
51
49
echo "==> $GOOS/$GOARCH"
二进制文件已变更,无法进行逐行预览。
二进制文件已变更,无法进行逐行预览。
二进制文件已变更,无法进行逐行预览。
@@ -0,0 +1,448 @@
1
package main
2
3
import (
4
"archive/tar"
5
"compress/gzip"
6
"crypto/sha256"
7
"encoding/hex"
8
"encoding/json"
9
"fmt"
10
"io"
11
"net/http"
12
"os"
13
"path/filepath"
14
"runtime"
15
"strings"
16
"time"
17
)
18
19
// RuntimeManifest mirrors runtime.json: one download source per platform.
20
type RuntimeManifest struct {
21
Version int `json:"version"`
22
Python string `json:"python"`
23
PBSTag string `json:"pbs_tag"`
24
Platforms map[string]RuntimeSpec `json:"platforms"`
25
}
26
27
// RuntimeSpec is a single platform's runtime download.
28
type RuntimeSpec struct {
29
Kind string `json:"kind"` // "pbs" (install_only tarball) or "android"
30
Arch string `json:"arch"`
31
URL string `json:"url"`
32
Size int64 `json:"size"`
33
SHA256 string `json:"sha256"`
34
}
35
36
// runtimeManifestKey returns the runtime.json entry used for this host:
37
// "linux-x64", "linux-arm64", "windows-amd64", "darwin-*" or "android".
38
func runtimeManifestKey() string {
39
if runtime.GOOS == "android" {
40
return "android"
41
}
42
arch := goArchToken()
43
switch runtime.GOOS {
44
case "linux":
45
if arch == "arm64" {
46
return "linux-arm64"
47
}
48
return "linux-x64"
49
case "windows":
50
return "windows-amd64"
51
case "darwin":
52
if arch == "arm64" {
53
return "darwin-arm64"
54
}
55
return "darwin-x64"
56
}
57
return runtime.GOOS + "-" + arch
58
}
59
60
// runtimeSpecForHost picks the manifest entry for this OS/arch.
61
func runtimeSpecForHost(m *RuntimeManifest) (*RuntimeSpec, error) {
62
key := runtimeManifestKey()
63
spec, ok := m.Platforms[key]
64
if !ok {
65
return nil, fmt.Errorf("no runtime in manifest for platform %q", key)
66
}
67
return &spec, nil
68
}
69
70
// partName is the temp file used while a download is in flight.
71
func partName(binDir string) string {
72
return filepath.Join(binDir, ".g4f-runtime", "runtime.download")
73
}
74
75
// installedOkName marks a fully verified runtime extraction.
76
func installedOkName(binDir string) string {
77
return filepath.Join(binDir, ".g4f-runtime", ".runtime-ok")
78
}
79
80
// downloadRuntime fetches the manifest URL for this host into cachePath
81
// (verifying size + sha256 when pinned) and reports live progress to stderr.
82
func downloadRuntime(binDir, cachePath string, spec *RuntimeSpec) error {
83
if err := os.MkdirAll(filepath.Dir(partName(binDir)), 0o755); err != nil {
84
return err
85
}
86
87
// Cache is valid when file exists with the pinned size (or any size when
88
// the manifest has no pinned size/sha).
89
if fi, err := os.Stat(cachePath); err == nil && fi.Size() > 0 {
90
if spec.Size <= 0 || fi.Size() == spec.Size {
91
fmt.Printf("runtime: using cached download (%s)\n", humanBytes(fi.Size()))
92
return verifyRuntime(cachePath, spec)
93
}
94
fmt.Printf("runtime: cached download incomplete, re-downloading\n")
95
}
96
97
fmt.Printf("runtime: downloading CPython %s (%s)\n", "3.14.7", humanBytes(spec.Size))
98
fmt.Printf(" %s\n", spec.URL)
99
start := time.Now()
100
101
out, err := os.Create(partName(binDir))
102
if err != nil {
103
return err
104
}
105
// Network http.Client with no default timeout: progress is what keeps the
106
// user informed, not a hard cutoff.
107
client := &http.Client{}
108
resp, err := client.Get(spec.URL)
109
if err != nil {
110
out.Close()
111
os.Remove(partName(binDir))
112
return fmt.Errorf("download failed: %w", err)
113
}
114
defer resp.Body.Close()
115
if resp.StatusCode != http.StatusOK {
116
out.Close()
117
os.Remove(partName(binDir))
118
return fmt.Errorf("download failed: HTTP %s", resp.Status)
119
}
120
121
// Prefer Content-Length; fall back to manifest size.
122
total := resp.ContentLength
123
if total <= 0 {
124
total = spec.Size
125
}
126
_, copyErr := copyWithProgress(out, resp.Body, total, start)
127
if cerr := out.Close(); copyErr == nil {
128
copyErr = cerr
129
}
130
if copyErr != nil {
131
os.Remove(partName(binDir))
132
return fmt.Errorf("download interrupted: %w", copyErr)
133
}
134
if err := os.Rename(partName(binDir), cachePath); err != nil {
135
os.Remove(partName(binDir))
136
return err
137
}
138
fmt.Printf("runtime: downloaded %s in %s\n", humanBytes(total), time.Since(start).Round(time.Second))
139
140
if spec.Size > 0 {
141
fi, err := os.Stat(cachePath)
142
if err != nil {
143
return err
144
}
145
if fi.Size() != spec.Size {
146
return fmt.Errorf("size mismatch: got %d, expected %d (update runtime.json)", fi.Size(), spec.Size)
147
}
148
}
149
return verifyRuntime(cachePath, spec)
150
}
151
152
// verifyRuntime validates sha256 when pinned in the manifest.
153
func verifyRuntime(cachePath string, spec *RuntimeSpec) error {
154
if spec.SHA256 == "" {
155
return nil // unpinned; trust size/transport
156
}
157
f, err := os.Open(cachePath)
158
if err != nil {
159
return err
160
}
161
defer f.Close()
162
h := sha256.New()
163
if _, err := io.Copy(h, f); err != nil {
164
return err
165
}
166
got := hex.EncodeToString(h.Sum(nil))
167
if !strings.EqualFold(got, spec.SHA256) {
168
return fmt.Errorf("sha256 mismatch: got %s, want %s", got, spec.SHA256)
169
}
170
fmt.Println("runtime: sha256 verified")
171
return nil
172
}
173
174
// copyWithProgress streams r into w while printing a \r-updated progress bar.
175
func copyWithProgress(w io.Writer, r io.Reader, total int64, start time.Time) (int64, error) {
176
buf := make([]byte, 256*1024)
177
var written int64
178
lastPrint := time.Time{}
179
for {
180
n, err := r.Read(buf)
181
if n > 0 {
182
if _, werr := w.Write(buf[:n]); werr != nil {
183
return written, werr
184
}
185
written += int64(n)
186
// Throttle progress output to ~5 updates/sec.
187
if time.Since(lastPrint) > 200*time.Millisecond {
188
lastPrint = time.Now()
189
progressLine(written, total, start)
190
}
191
}
192
if err == io.EOF {
193
break
194
}
195
if err != nil {
196
return written, err
197
}
198
}
199
// Final line: clear the \r-update with a newline.
200
progressLine(written, total, start)
201
return written, nil
202
}
203
204
// progressLine prints "pct | done/total | rate | eta" on one line (carriage
205
// return, no newline) so it reads like a live progress bar.
206
func progressLine(written, total int64, start time.Time) {
207
if total > 0 {
208
pct := float64(written) / float64(total) * 100
209
eta := time.Duration(float64(time.Since(start)) / (float64(written) / float64(total)) * (1 - float64(written)/float64(total)))
210
fmt.Printf("\r %5.1f%% %s / %s %s/s eta %s",
211
pct, humanBytes(written), humanBytes(total), humanBytes(int64(float64(written)/time.Since(start).Seconds())), eta.Round(time.Second))
212
} else {
213
fmt.Printf("\r %s downloaded", humanBytes(written))
214
}
215
}
216
217
// humanBytes renders byte counts readably (KiB/MiB/GiB).
218
func humanBytes(b int64) string {
219
const unit = 1024
220
if b < unit {
221
return fmt.Sprintf("%d B", b)
222
}
223
div, exp := int64(unit), 0
224
for n := b / unit; n >= unit; n /= unit {
225
div *= unit
226
exp++
227
}
228
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
229
}
230
231
// extractRuntime unpacks the downloaded archive into binDir/python-home/:
232
// - *.tar.gz (pbs, android) via gzip+tar with a single top dir stripped
233
// - *.zip (windows embed, legacy) via archive/zip
234
//
235
// The archive's single top-level directory (e.g. "python/", "prefix/") is
236
// stripped and the contents land in python-home/ (bin/..., lib/...),
237
// matching the launcher/lookup layout.
238
func extractRuntime(binDir, cachePath string) error {
239
dest := filepath.Join(binDir, "python-home")
240
if err := os.MkdirAll(dest, 0o755); err != nil {
241
return err
242
}
243
f, err := os.Open(cachePath)
244
if err != nil {
245
return err
246
}
247
defer f.Close()
248
249
if strings.HasSuffix(strings.ToLower(cachePath), ".zip") {
250
fi, err := f.Stat()
251
if err != nil {
252
return err
253
}
254
return extractZip(f, fi.Size(), dest)
255
}
256
257
gz, err := gzip.NewReader(f)
258
if err != nil {
259
return err
260
}
261
defer gz.Close()
262
tr := tar.NewReader(gz)
263
264
// Determine the single top-level directory.
265
var top string
266
for {
267
hdr, err := tr.Next()
268
if err == io.EOF {
269
break
270
}
271
if err != nil {
272
return err
273
}
274
clean := filepath.Clean(hdr.Name)
275
parts := strings.Split(clean, string(os.PathSeparator))
276
if len(parts) > 0 && parts[0] != "." && parts[0] != "" && top == "" {
277
top = parts[0]
278
}
279
}
280
281
// Second pass: extract everything with zip-slip protection.
282
if _, err := f.Seek(0, io.SeekStart); err != nil {
283
return err
284
}
285
gz, err = gzip.NewReader(f)
286
if err != nil {
287
return err
288
}
289
defer gz.Close()
290
tr = tar.NewReader(gz)
291
for {
292
hdr, err := tr.Next()
293
if err == io.EOF {
294
break
295
}
296
if err != nil {
297
return err
298
}
299
rel := hdr.Name
300
if top != "" {
301
rel = strings.TrimPrefix(rel, top+"/")
302
rel = strings.TrimPrefix(rel, top)
303
}
304
rel = strings.TrimPrefix(rel, "/")
305
rel = strings.TrimPrefix(rel, "./")
306
name := filepath.Clean(rel)
307
if name == "." || name == "" {
308
continue
309
}
310
if name == ".." || strings.HasPrefix(name, ".."+string(os.PathSeparator)) {
311
return fmt.Errorf("unsafe path in archive: %s", hdr.Name)
312
}
313
target := filepath.Join(dest, name)
314
if !strings.HasPrefix(target, filepath.Clean(dest)+string(os.PathSeparator)) {
315
return fmt.Errorf("unsafe path in archive: %s", hdr.Name)
316
}
317
318
switch hdr.Typeflag {
319
case tar.TypeDir:
320
if err := os.MkdirAll(target, 0o755); err != nil {
321
return err
322
}
323
case tar.TypeReg, tar.TypeRegA:
324
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
325
return err
326
}
327
out, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(hdr.Mode)&0o777)
328
if err != nil {
329
return err
330
}
331
if _, err := io.Copy(out, tr); err != nil {
332
out.Close()
333
return err
334
}
335
if err := out.Close(); err != nil {
336
return err
337
}
338
if err := os.Chmod(target, os.FileMode(hdr.Mode)&0o777); err != nil {
339
return err
340
}
341
case tar.TypeSymlink:
342
// Symlinks in pbs installs point within the tree; recreate them
343
// (libpython3.so -> libpython3.14.so etc).
344
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
345
return err
346
}
347
_ = os.Remove(target)
348
if err := os.Symlink(hdr.Linkname, target); err != nil {
349
// Some filesystems disallow symlinks; fall back to a copy.
350
_ = os.Remove(target)
351
if copySymlinkTarget(target, hdr.Linkname) != nil {
352
// Best effort: log-free skip keeps extraction robust.
353
_ = err
354
}
355
}
356
default:
357
// Hard links, devices, etc: skip silently (not needed).
358
}
359
}
360
return nil
361
}
362
363
// copySymlinkTarget attempts to copy a symlink's target for filesystems that
364
// reject symlinks (best effort).
365
func copySymlinkTarget(target, linkname string) error {
366
src := filepath.Join(filepath.Dir(target), linkname)
367
data, err := os.ReadFile(src)
368
if err != nil {
369
return err
370
}
371
return os.WriteFile(target, data, 0o755)
372
}
373
374
// ensureRuntime is the top-level entry point. It downloads (if needed) and
375
// extracts the platform runtime into binDir, then returns the python
376
// executable/launcher path.
377
func ensureRuntime() (string, error) {
378
binDir := installDir()
379
exe, err := pythonExecutable(binDir)
380
if err == nil {
381
if _, statErr := os.Stat(exe); statErr == nil {
382
return exe, nil
383
}
384
}
385
386
manifest, err := readRuntimeManifest()
387
if err != nil {
388
return "", err
389
}
390
spec, err := runtimeSpecForHost(manifest)
391
if err != nil {
392
return "", err
393
}
394
395
cachePath := filepath.Join(binDir, ".g4f-runtime", "runtime-"+filepath.Base(spec.URL))
396
if err := downloadRuntime(binDir, cachePath, spec); err != nil {
397
return "", err
398
}
399
400
// Extract unless already done (stamp written after successful extract).
401
okStamp := installedOkName(binDir)
402
if _, err := os.Stat(okStamp); err != nil {
403
fmt.Println("runtime: extracting (this can take a minute)...")
404
start := time.Now()
405
if err := extractRuntime(binDir, cachePath); err != nil {
406
return "", fmt.Errorf("extract runtime: %w", err)
407
}
408
if err := os.WriteFile(okStamp, []byte("ok"), 0o644); err != nil {
409
return "", err
410
}
411
fmt.Printf("runtime: extracted in %s\n", time.Since(start).Round(time.Second))
412
}
413
414
return finalizeRuntime(binDir)
415
}
416
417
// finalizeRuntime does platform-specific finishing (launcher setup) and
418
// returns the interpreter path.
419
func finalizeRuntime(binDir string) (string, error) {
420
// Android builds the dlopen C runner instead of a shell launcher.
421
if runtime.GOOS == "android" {
422
return finalizeAndroidRuntime(binDir)
423
}
424
// Write the unix launcher (windows uses python.exe from the archive).
425
if err := writeLauncher(binDir); err != nil {
426
return "", err
427
}
428
exe, err := pythonExecutable(binDir)
429
if err != nil {
430
return "", err
431
}
432
if _, err := os.Stat(exe); err != nil {
433
return "", fmt.Errorf("python runtime extracted but %s is missing", exe)
434
}
435
return exe, nil
436
}
437
438
// parseRuntimeManifest decodes a RuntimeManifest from bytes.
439
func parseRuntimeManifest(data []byte) (*RuntimeManifest, error) {
440
var m RuntimeManifest
441
if err := json.Unmarshal(data, &m); err != nil {
442
return nil, err
443
}
444
if len(m.Platforms) == 0 {
445
return nil, fmt.Errorf("runtime.json: no platforms defined")
446
}
447
return &m, nil
448
}
@@ -1,181 +1,71 @@
1
1
#!/usr/bin/env bash
2
2
# fetch-python.sh
3
3
#
4
# Downloads the CPython embeddable runtime for every supported platform,
5
# merges gpt4free into it (source + prebuilt wheels), patches python311._pth
6
# so the interpreter is fully relocatable, and stores the result as a zip
7
# next to embed/<os>/EMPTY_PYTHON_RUNTIME so `go:embed` picks it up.
4
# Downloads the CPython runtimes for every supported platform and pins their
5
# size + sha256 into runtime.json. The g4f-go binary itself does NOT embed
6
# the runtimes anymore: it downloads the matching archive on first run (with
7
# live progress feedback) and verifies it against this manifest.
8
8
#
9
9
# Usage:
10
# ./fetch-python.sh # all platforms (linux, windows, darwin, bsd)
11
# ./fetch-python.sh linux # single platform
12
# G4F_VERSION=0.4.x ./fetch-python.sh
10
# ./fetch-python.sh # download all platforms
11
# ./fetch-python.sh linux # single platform group (linux|windows|darwin|android)
13
12
#
14
# Requirements: bash, curl, unzip, zip, python3 (with pip)
13
# Requirements: bash, curl, python3 (for sha256/size pinning)
14
#
15
# After updating URLs in runtime.json, re-run this to refresh the pins.
15
16
16
17
set -euo pipefail
17
18
18
19
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
PYVER="3.14.7"
20
PYTAG="cp314"
21
PYABI="cp314-cp314"
22
PBS_TAG="${PBS_TAG:-20260805}"
23
PBS_BASE="https://github.com/astral-sh/python-build-standalone/releases/download/${PBS_TAG}"
24
PYORG_BASE="https://www.python.org/ftp/python/${PYVER}"
25
G4F_SRC="${G4F_SRC:-$HERE/..}" # gpt4free repository root
26
G4F_VERSION="${G4F_VERSION:-$(cd "$G4F_SRC" && python3 -c 'import sys;sys.path.insert(0,"g4f");from version import __version__;print(__version__)' 2>/dev/null || echo 0.4.x)}"
27
28
# Minimum versions for deps that have ancient pure-python releases on PyPI.
29
# `pip download --platform <x>` considers py3-none-any wheels valid for any
30
# target, so an old/misbehaving resolver can pick e.g. aiohttp 0.13.1 (2015,
31
# predates async/await) which crashes the embedded CPython 3.14. Pinning
32
# floors here keeps the offline wheel set sane.
33
WHEEL_FLOORS="aiohttp>=3.8"
34
35
WORK="$(mktemp -d)"
36
trap 'rm -rf "$WORK"' EXIT
37
38
mkdir -p "$HERE/embed/linux" "$HERE/embed/windows" "$HERE/embed/darwin" "$HERE/embed/bsd"
39
40
# Build the g4f project wheel ONCE (pure python, platform-independent).
41
mkdir -p "$WORK/wheels-cache"
42
(cd "$G4F_SRC" && python3 -m pip wheel . -w "$WORK/wheels-cache" --no-deps -q 2>/dev/null || true)
20
cd "$HERE"
21
MANIFEST="runtime.json"
43
22
44
# platform source spec goos goarch name
45
PLATFORMS=(
46
"linux pbs x86_64-unknown-linux-gnu linux amd64 linux-x64"
47
"linux pbs aarch64-unknown-linux-gnu linux arm64 linux-arm64"
48
"windows pbs x86_64-pc-windows-msvc windows amd64 windows-amd64"
49
"windows pbs i686-pc-windows-msvc windows 386 windows-x86"
50
"windows pbs aarch64-pc-windows-msvc windows arm64 windows-arm64"
51
"darwin pbs aarch64-apple-darwin darwin arm64 darwin-arm64"
52
"darwin pbs x86_64-apple-darwin darwin amd64 darwin-x64"
53
"bsd pbs x86_64-unknown-linux-gnu freebsd amd64 bsd-amd64"
54
)
55
WANT=("$@"); [ ${#WANT[@]} -eq 0 ] && WANT=(linux windows darwin bsd)
56
57
do_platform() {
58
local plat src spec goos goarch name
59
read -r plat src spec goos goarch name <<<"$1"
60
[[ " ${WANT[*]} " == *" $plat "* ]] || return 0
61
echo "==> [$plat/$goarch] $name ($src)"
62
63
local dir="$WORK/$name"
64
mkdir -p "$dir"
65
local home="$dir/python-home"
66
if [ "$src" = "pbs" ]; then
67
local asset="cpython-${PYVER}+${PBS_TAG}-${spec}-install_only.tar.gz"
68
curl -fsSL -o "$dir/base.tar.gz" "$PBS_BASE/$asset"
69
mkdir -p "$home"
70
# python-build-standalone is a full installable layout (bin/ or top-level
71
# python.exe) that is already relocatable, so no _pth patching is needed.
72
tar -xzf "$dir/base.tar.gz" -C "$home" --strip-components=1
73
rm -f "$dir/base.tar.gz"
74
# On unix the entry point is `bin/python3`; on windows `python.exe`.
75
if [ -f "$home/bin/python3" ]; then
76
mv "$home/bin/python3" "$home/bin/python"
77
find "$home/bin" -maxdepth 1 -name 'python3.*' -exec rm -f {} +
78
fi
79
else
80
local basezip="python-${PYVER}-${spec}.zip"
81
curl -fsSL -o "$dir/base.zip" "$PYORG_BASE/$basezip"
82
( cd "$dir" && unzip -oq "$basezip" )
83
# 1) Relocatable: strip the hard-coded prefix path, keep python314.zip.
84
local pth
85
pth=$(find "$dir" -maxdepth 1 -name 'python3*._pth' | head -1)
86
sed -i 's|^#import site|import site|' "$pth" || true
87
grep -v '^\.\./' "$pth" > "$dir/py.pth" || true
88
printf '\n..\\python-home\n..\\wheels\n' >> "$dir/py.pth"
89
mv "$dir/py.pth" "$pth"
90
fi
91
92
# 2) Download wheels for THIS target platform/arch only, then merge the
93
# g4f package + wheels so the interpreter is self-contained.
94
# pip --platform lets us fetch the right cp314 wheels from a single
95
# ubuntu runner for every target (no need to execute the target python).
96
local tag
97
case "$name" in
98
linux-x64) tag="manylinux2014_x86_64" ;;
99
linux-arm64) tag="manylinux2014_aarch64" ;;
100
windows-amd64) tag="win_amd64" ;;
101
windows-x86) tag="win32" ;;
102
windows-arm64) tag="win_arm64" ;;
103
darwin-arm64) tag="macosx_11_0_arm64" ;;
104
darwin-x64) tag="macosx_10_15_universal2" ;; # covers x86_64 + arm64
105
bsd-amd64) tag="manylinux2014_x86_64" ;;
106
*) echo " WARNING: no wheel tag for $name; runtime will need network on first run" >&2; tag="" ;;
107
esac
108
mkdir -p "$dir/wheels"
109
# if [ -n "$tag" ]; then
110
# # Windows pip can't read process-substitution FDs from its subprocess, so
111
# # write the version floors to a real file once per platform.
112
# local floors="$dir/wheels.floors"
113
# printf '%s\n' "$WHEEL_FLOORS" > "$floors"
114
# # Full dependency resolution (no --no-deps) so the runtime's offline
115
# # `pip install g4f` finds every transitive wheel it needs.
116
# # brotli is optional in g4f and has no cp314 wheel for win_arm64;
117
# # fall back to fetching everything else if a single dep is unavailable.
118
# if ! python3 -m pip download \
119
# -r "$G4F_SRC/requirements-min.txt" \
120
# --constraint "$floors" \
121
# --only-binary=:all: \
122
# --python-version "$PYVER" --implementation cp --abi "cp$(echo "$PYVER" | tr -d '.')" \
123
# --platform "$tag" \
124
# -d "$dir/wheels" -q 2>"$dir/pip.err"; then
125
# echo " WARNING: full wheel set for $name not available; retrying without brotli (optional dep)" >&2
126
# grep -v '^brotli$' "$G4F_SRC/requirements-min.txt" > "$dir/req-nobrotli.txt"
127
# python3 -m pip download \
128
# -r "$dir/req-nobrotli.txt" \
129
# --constraint "$floors" \
130
# --only-binary=:all: \
131
# --python-version "$PYVER" --implementation cp --abi "cp$(echo "$PYVER" | tr -d '.')" \
132
# --platform "$tag" \
133
# -d "$dir/wheels" -q 2>>"$dir/pip.err" || {
134
# echo " WARNING: wheel download for $name failed; runtime will need network on first run" >&2
135
# }
136
# fi
137
# # Belt-and-braces: drop any wheel whose Requires-Python metadata excludes
138
# # our interpreter. The --constraint above prevents this at resolve time;
139
# # this catches stale wheels (e.g. copied in from wheels-cache) and covers
140
# # ancient pure-python releases like aiohttp 0.13.1.
141
# python3 - "$dir/wheels" "$PYVER" <<'PY' || true
142
# import glob, os, sys, zipfile
143
# from packaging.specifiers import SpecifierSet
144
# from packaging.version import Version
145
# want_v = Version(sys.argv[2])
146
# for whl in glob.glob(os.path.join(sys.argv[1], "*.whl")):
147
# try:
148
# with zipfile.ZipFile(whl) as z:
149
# meta = next((n for n in z.namelist() if n.endswith(".dist-info/METADATA")), None)
150
# if not meta:
151
# continue
152
# txt = z.read(meta).decode("utf-8", "replace")
153
# rp = next((l.split(":", 1)[1].strip() for l in txt.splitlines()
154
# if l.lower().startswith("requires-python:")), None)
155
# if rp and not SpecifierSet(rp).contains(want_v):
156
# print(f" removing {os.path.basename(whl)} (Requires-Python {rp} excludes {sys.argv[2]})")
157
# os.remove(whl)
158
# except Exception as e:
159
# print(f" skip check {os.path.basename(whl)}: {e}")
160
# PY
161
# fi
23
pick_urls() {
24
python3 - "$MANIFEST" "$WANT" <<'PY'
25
import json, sys
26
m = json.load(open(sys.argv[1]))
27
want = sys.argv[2]
28
for name, spec in m["platforms"].items():
29
if want == "all" or name.startswith(want):
30
print(name, spec["url"])
31
PY
32
}
162
33
163
# 3) Merge g4f package + wheels so the interpreter is self-contained.
164
# rsync -a --exclude='.git' --exclude='g4f-go' --exclude='g4f.dev' --exclude='g4f.egg-info' \
165
# "$G4F_SRC/g4f" "$dir/python-home/g4f"
166
# rsync -a --exclude='.git' --exclude='g4f-go' --exclude='g4f.dev' --exclude='g4f.egg-info' \
167
# "$G4F_SRC/requirements-min.txt" "$dir/python-home/requirements-min.txt"
168
# cp -n "$WORK/wheels-cache/"*.whl "$dir/wheels/" 2>/dev/null || true
34
WANT="${1:-all}"
35
case "$WANT" in
36
linux|windows|darwin|android|all) ;;
37
*) echo "Unknown filter: $WANT (expected linux|windows|darwin|android|all)" >&2; exit 1 ;;
38
esac
169
39
170
# 4) Pre-stamp install so first run is instant.
171
mkdir -p "$dir/.g4f-runtime"
172
printf 'g4f %s embedded (CPython %s)\n' "$G4F_VERSION" "$PYVER" > "$dir/.g4f-runtime/.installed"
40
if ! command -v python3 >/dev/null; then
41
echo "python3 required for manifest pinning" >&2; exit 1
42
fi
173
43
174
# 5) Repack next to the placeholder so go:embed sees only one zip.
175
rm -f "$HERE/embed/$plat/${name}-embed-${PYVER}.zip"
176
( cd "$WORK" && zip -qr "$HERE/embed/$plat/${name}-embed-${PYVER}.zip" "$name" )
177
echo " -> embed/$plat/${name}-embed-${PYVER}.zip"
44
pin() {
45
# pin <name> <url>: download, compute sha256+size, update runtime.json
46
local name url file sha size
47
name="$1"; url="$2"
48
file="$(mktemp)"
49
echo "==> [$name] downloading $url"
50
curl -fsSL -o "$file" "$url"
51
sha="$(sha256sum "$file" | cut -d' ' -f1)"
52
size="$(stat -c%s "$file")"
53
rm -f "$file"
54
echo " sha256=$sha"
55
echo " size=$size"
56
python3 - "$MANIFEST" "$name" "$sha" "$size" <<'PY'
57
import json, sys
58
path, name, sha, size = sys.argv[1:5]
59
m = json.load(open(path))
60
m["platforms"][name]["sha256"] = sha
61
m["platforms"][name]["size"] = int(size)
62
json.dump(m, open(path, "w"), indent=2, sort_keys=True)
63
print(" -> runtime.json updated for", name)
64
PY
178
65
}
179
66
180
for p in "${PLATFORMS[@]}"; do do_platform "$p"; done
181
echo "Done. Rebuild with ./build-all.sh (or: go build -o g4f-go .)"
67
pick_urls | while read -r name url; do
68
pin "$name" "$url"
69
done
70
71
echo "Done. Build with ./build-all.sh (or: go build -o g4f-go .)"
@@ -5,29 +5,29 @@ import (
5
5
"fmt"
6
6
"os"
7
7
"os/signal"
8
"syscall"
9
8
"path/filepath"
10
9
"strings"
10
"syscall"
11
11
"time"
12
12
)
13
13
14
// main handles the `g4f-go <command>` interface. Anything unknown is forwarded
15
// to the embedded g4f CLI (so `g4f-go client "hello"` just works).
14
// printHelp shows the g4f-go usage.
16
15
func printHelp() {
17
fmt.Printf(`g4f-go %s - gpt4free with embedded Python %s
16
fmt.Printf(`g4f-go %s - gpt4free with a downloaded CPython %s runtime
18
17
19
18
Usage:
20
19
g4f-go <g4f args...> run gpt4free (e.g. g4f-go client "hello")
21
20
g4f-go api --port 8080 start the OpenAI-compatible API server
22
21
g4f-go gui launch the web GUI
23
g4f-go install g4f (re)install the g4f package into the embedded runtime
24
g4f-go status show runtime status
22
g4f-go status show runtime download/install status
23
g4f-go install g4f (re)install the g4f package (network)
24
g4f-go bootstrap refresh the g4f package installation
25
g4f-go --version print version
25
26
g4f-go help show this help
26
g4f-go --version show version
27
27
28
Environment:
29
G4F_PYTHON_ONLY=1 print the embedded python path and exit (for wrappers)
30
`, Version, PythonVer)
28
The CPython runtime downloads on first run (with progress feedback) into
29
%s. Set G4F_PYTHON_ONLY=1 to print the interpreter path and exit.
30
`, Version, PythonVer, installDir())
31
31
}
32
32
33
33
func main() {
@@ -56,19 +56,21 @@ func runMain() int {
56
56
printHelp()
57
57
return 0
58
58
case "--version", "-v":
59
fmt.Printf("g4f-go %s (embedded CPython %s)\n", Version, PythonVer)
59
fmt.Printf("g4f-go %s (CPython %s)\n", Version, PythonVer)
60
60
return 0
61
}
62
63
switch args[0] {
61
64
case "status":
62
exe := pythonExecutable(binDir)
63
65
stamp := filepath.Join(binDir, ".g4f-runtime", ".installed")
64
66
fmt.Printf("binary dir: %s\n", binDir)
65
fmt.Printf("python: %s\n", exe)
66
if _, err := os.Stat(exe); err == nil {
67
fmt.Println("runtime: extracted")
67
fmt.Printf("python: %s\n", py)
68
if _, serr := os.Stat(py); serr == nil {
69
fmt.Println("runtime: downloaded & extracted")
68
70
} else {
69
fmt.Println("runtime: not extracted (will extract on first run)")
71
fmt.Println("runtime: not downloaded yet (will download on first run)")
70
72
}
71
if _, err := os.Stat(stamp); err == nil {
73
if _, serr := os.Stat(stamp); serr == nil {
72
74
fmt.Println("g4f: installed")
73
75
} else {
74
76
fmt.Println("g4f: not installed (will install on first run)")
@@ -76,18 +78,15 @@ func runMain() int {
76
78
code, err := runPython(ctx, py, []string{"--version"})
77
79
if err != nil {
78
80
fmt.Fprintln(os.Stderr, "g4f-go:", err)
79
return 1
80
81
}
81
82
return code
82
83
case "install", "uninstall":
83
84
if len(args) < 2 {
84
85
fmt.Fprintln(os.Stderr, "usage: g4f-go install g4f")
85
return 2
86
86
}
87
87
code, err := runPython(ctx, py, append([]string{"-m", "pip"}, args...))
88
88
if err != nil {
89
89
fmt.Fprintln(os.Stderr, "g4f-go:", err)
90
return 1
91
90
}
92
91
return code
93
92
case "bootstrap":
@@ -95,7 +94,6 @@ func runMain() int {
95
94
code, err := runPython(ctx, py, []string{"-m", "pip", "install", "--no-input", "g4f[slim]"}, pipEnv(binDir)...)
96
95
if err != nil {
97
96
fmt.Fprintln(os.Stderr, "g4f-go:", err)
98
return 1
99
97
}
100
98
return code
101
99
}
@@ -105,18 +103,20 @@ func runMain() int {
105
103
return 0
106
104
}
107
105
108
exe := pythonExecutable(binDir)
106
exe, err := pythonExecutable(binDir)
107
if err != nil {
108
fmt.Fprintln(os.Stderr, "g4f-go:", err)
109
return 1
110
}
109
111
start := time.Now()
110
112
if err := installG4F(binDir, exe, start); err != nil {
111
113
fmt.Fprintln(os.Stderr, "g4f-go:", err)
112
return 1
113
114
}
114
115
115
116
// Default: forward everything to the g4f module.
116
117
code, err := runPython(ctx, py, append([]string{"-m", "g4f"}, args...))
117
118
if err != nil {
118
119
fmt.Fprintln(os.Stderr, "g4f-go:", err)
119
return 1
120
120
}
121
121
return code
122
122
}
@@ -0,0 +1,23 @@
1
package main
2
3
import (
4
_ "embed"
5
"fmt"
6
)
7
8
//go:embed runtime.json
9
var embeddedManifest []byte
10
11
func init() {
12
// Validate at startup: a malformed runtime.json would otherwise only
13
// surface on first download.
14
if _, err := parseRuntimeManifest(embeddedManifest); err != nil {
15
panic(fmt.Sprintf("g4f-go: embedded runtime.json is invalid: %v", err))
16
}
17
}
18
19
// readRuntimeManifest is overridden by the embedded copy so releases work
20
// without a runtime.json next to the binary.
21
func readRuntimeManifest() (*RuntimeManifest, error) {
22
return parseRuntimeManifest(embeddedManifest)
23
}
@@ -13,11 +13,13 @@ import (
13
13
)
14
14
15
15
// pythonLauncher is the shell wrapper that sets PYTHONHOME etc. and execs the
16
// embedded python. Written next to the binary during extraction.
16
// downloaded python. Written next to the binary after the runtime is ready.
17
// Note: it must never exec itself ($DIR/python is the wrapper); the real
18
// interpreter always lives at $DIR/python-home/bin/python on unix.
17
19
const pythonLauncher = `#!/bin/sh
18
# g4f-go launcher for the embedded CPython runtime.
20
# g4f-go launcher for the downloaded CPython runtime.
19
21
DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
20
if [ -x "$DIR/python" ]; then PY="$DIR/python"; else PY="$DIR/python-home/bin/python"; fi
22
PY="$DIR/python-home/bin/python"
21
23
export PYTHONHOME="$DIR/python-home"
22
24
export PYTHONNOUSERSITE=1
23
25
export PYTHONDONTWRITEBYTECODE=1
@@ -122,36 +124,47 @@ func extractZip(r io.ReaderAt, size int64, dest string) error {
122
124
return nil
123
125
}
124
126
125
// pythonExecutable returns the launcher (unix) or python.exe (windows) path.
127
// pythonExecutable returns the python binary (or launcher) path for a
128
// downloaded runtime, or an error when no interpreter is present yet.
126
129
//
127
// The embedded archive is laid out as <name>/python-home/<exe>, so after
128
// extraction the interpreter lives at binDir/python-home/python.exe on
129
// windows and binDir/python-home/bin/python on unix. We prefer that location
130
// and fall back to the legacy binDir/python(.exe) layout produced by older
131
// archives, mirroring the shell launcher's logic.
132
func pythonExecutable(binDir string) string {
130
// pbs installs land in binDir/python-home/ with bin/python (unix) or
131
// python.exe (windows). The android build overrides this with a C runner
132
// that dlopens libpython (see runtime_android.go). Note that binDir/python
133
// is the *shell wrapper* (never a real interpreter), so it is not a valid
134
// fallback here.
135
func pythonExecutable(binDir string) (string, error) {
136
if exe, err := androidPythonExecutable(binDir); exe != "" || err != nil {
137
return exe, err
138
}
133
139
home := filepath.Join(binDir, "python-home")
140
var candidates []string
134
141
if runtime.GOOS == "windows" {
135
return filepath.Join(home, "python.exe")
142
candidates = []string{filepath.Join(home, "python.exe")}
143
} else {
144
candidates = []string{filepath.Join(home, "bin", "python")}
136
145
}
137
exe := filepath.Join(home, "bin", "python")
138
if fi, err := os.Stat(exe); err == nil && !fi.IsDir() {
139
return exe
146
for _, exe := range candidates {
147
if fi, err := os.Stat(exe); err == nil && !fi.IsDir() {
148
return exe, nil
149
}
140
150
}
141
return filepath.Join(binDir, "python")
151
return "", fmt.Errorf("no python executable found (runtime not downloaded?)")
142
152
}
143
153
144
// pythonHome returns the extracted interpreter root.
154
// pythonHome returns the downloaded interpreter root.
145
155
func pythonHome(binDir string) string {
156
if h := androidPythonHome(binDir); h != "" {
157
return h
158
}
146
159
return filepath.Join(binDir, "python-home")
147
160
}
148
161
149
// writeLauncher installs the unix shell wrapper after extraction.
162
// writeLauncher installs the unix shell wrapper after the runtime is ready.
150
163
func writeLauncher(binDir string) error {
151
164
if runtime.GOOS == "windows" {
152
165
return nil
153
166
}
154
path := pythonExecutable(binDir)
167
path := filepath.Join(binDir, "python")
155
168
if err := os.WriteFile(path, []byte(pythonLauncher), 0o755); err != nil {
156
169
return err
157
170
}
@@ -0,0 +1,50 @@
1
{
2
"version": 1,
3
"python": "3.14.7",
4
"pbs_tag": "20260805",
5
"comment": "Runtime archives are downloaded at first run (never embedded in the binary). pbs = python-build-standalone (astral-sh), pyorg = python.org. SHA256 listed as empty must be filled after downloading (curl ... | sha256sum) and re-pinned.",
6
"platforms": {
7
"android": {
8
"kind": "android",
9
"arch": "any",
10
"url": "https://www.python.org/ftp/python/3.14.7/python-3.14.7-aarch64-linux-android.tar.gz",
11
"size": 22670000,
12
"sha256": ""
13
},
14
"linux-x64": {
15
"kind": "pbs",
16
"arch": "amd64",
17
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.14.7+20260805-x86_64-unknown-linux-gnu-install_only.tar.gz",
18
"size": 0,
19
"sha256": ""
20
},
21
"linux-arm64": {
22
"kind": "pbs",
23
"arch": "arm64",
24
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.14.7+20260805-aarch64-unknown-linux-gnu-install_only.tar.gz",
25
"size": 0,
26
"sha256": ""
27
},
28
"windows-amd64": {
29
"kind": "pbs",
30
"arch": "amd64",
31
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.14.7+20260805-x86_64-pc-windows-msvc-install_only.tar.gz",
32
"size": 0,
33
"sha256": ""
34
},
35
"darwin-x64": {
36
"kind": "pbs",
37
"arch": "amd64",
38
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.14.7+20260805-x86_64-apple-darwin-install_only.tar.gz",
39
"size": 0,
40
"sha256": ""
41
},
42
"darwin-arm64": {
43
"kind": "pbs",
44
"arch": "arm64",
45
"url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260805/cpython-3.14.7+20260805-aarch64-apple-darwin-install_only.tar.gz",
46
"size": 0,
47
"sha256": ""
48
}
49
}
50
}
@@ -173,6 +173,7 @@ class DeepInfra(OpenaiTemplate):
173
173
url = "https://deepinfra.com"
174
174
login_url = "https://deepinfra.com/dash/api_keys"
175
175
base_url = "https://api.deepinfra.com/v1/openai"
176
backup_url = "https://api.deepinfra.com/v1/openai"
176
177
177
178
working = True
178
179
active_by_default = True
@@ -210,7 +211,7 @@ class DeepInfra(OpenaiTemplate):
210
211
async def create_async_generator(
211
212
cls, model, messages, api_key=None, headers=None, **kwargs
212
213
):
213
if not api_key:
214
if not api_key or not cls.is_provider_api_key(api_key):
214
215
# Generate a Turnstile token for each request (required without an API key)
215
216
token = await get_turnstile_token_async(model)
216
217
if token:
@@ -231,9 +232,11 @@ class DeepInfra(OpenaiTemplate):
231
232
def get_headers(
232
233
cls, stream: bool, api_key: str = None, headers: dict = None
233
234
) -> dict:
234
headers = super().get_headers(stream, api_key, headers)
235
if not api_key:
236
headers["X-Deepinfra-Source"] = "web-page"
235
if not api_key or not cls.is_provider_api_key(api_key):
236
if headers is None:
237
headers = {}
238
headers["X-DeepInfra-Source"] = "web-page"
237
239
headers["Origin"] = "https://deepinfra.com"
238
240
headers["Referer"] = "https://deepinfra.com/"
239
return headers
241
api_key = None
242
return super().get_headers(stream, api_key, headers)
@@ -336,7 +336,7 @@ class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin
336
336
return {
337
337
"Accept": "text/event-stream" if stream else "application/json",
338
338
"Content-Type": "application/json",
339
**({"Authorization": f"Bearer {api_key}"} if api_key else {}),
339
# **({"Authorization": f"Bearer {api_key}"} if api_key else {}),
340
340
**({} if headers is None else headers),
341
341
}
342
342