mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-22 15:49:12 -04:00
feat: send a LocalAI User-Agent on registry pulls (#10434)
LocalAI pulls models from OCI registries (via go-containerregistry), the Ollama registry, and OCI blob stores (via oras), but every request went out with the underlying library's generic User-Agent, so registry operators had no way to attribute traffic to LocalAI. Add an oci.UserAgent() helper that returns "LocalAI" (or "LocalAI/<version>" when the binary is built with a version stamp via internal.Version) and wire it into all three pull paths: - pkg/oci/image.go: remote.WithUserAgent on the go-containerregistry image and digest requests - pkg/oci/ollama.go: a User-Agent header on the Ollama manifest request - pkg/oci/blob.go: a LocalAI User-Agent on the oras blob client. This mirrors oras' auth.DefaultClient (same retry.DefaultClient policy); only the advertised User-Agent changes. Implements #6258. Assisted-by: Claude:claude-opus-4-8 golangci-lint Signed-off-by: Vijay Sai <vijaysaijnv@gmail.com>
This commit is contained in:
@@ -131,6 +131,10 @@ local-ai run ollama://gemma:2b
|
||||
local-ai run oci://localai/phi-2:latest
|
||||
```
|
||||
|
||||
{{% notice note %}}
|
||||
When pulling models from Ollama or OCI registries, LocalAI identifies itself with a `LocalAI/<version>` `User-Agent` header so registry operators can attribute usage to LocalAI.
|
||||
{{% /notice %}}
|
||||
|
||||
### Run Models via URI
|
||||
|
||||
To run models via URI, specify a URI to a model file or a configuration file when starting LocalAI. Valid syntax includes:
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
|
||||
oras "oras.land/oras-go/v2"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
"oras.land/oras-go/v2/registry/remote/auth"
|
||||
"oras.land/oras-go/v2/registry/remote/retry"
|
||||
)
|
||||
|
||||
func FetchImageBlob(ctx context.Context, r, reference, dst string, statusReader func(ocispec.Descriptor) io.Writer) error {
|
||||
@@ -28,6 +30,16 @@ func FetchImageBlob(ctx context.Context, r, reference, dst string, statusReader
|
||||
}
|
||||
repo.SkipReferrersGC = true
|
||||
|
||||
// Identify LocalAI to the registry. This mirrors oras' auth.DefaultClient
|
||||
// (same retry policy) but advertises a LocalAI User-Agent instead of the
|
||||
// library default.
|
||||
client := &auth.Client{
|
||||
Client: retry.DefaultClient,
|
||||
Cache: auth.NewCache(),
|
||||
}
|
||||
client.SetUserAgent(UserAgent())
|
||||
repo.Client = client
|
||||
|
||||
// https://github.com/oras-project/oras/blob/main/cmd/oras/internal/option/remote.go#L364
|
||||
// https://github.com/oras-project/oras/blob/main/cmd/oras/root/blob/fetch.go#L136
|
||||
desc, reader, err := oras.Fetch(ctx, repo.Blobs(), reference, oras.DefaultFetchOptions)
|
||||
|
||||
@@ -176,6 +176,7 @@ func GetImage(targetImage, targetPlatform string, auth *registrytypes.AuthConfig
|
||||
opts := []remote.Option{
|
||||
remote.WithTransport(tr),
|
||||
remote.WithPlatform(*platform),
|
||||
remote.WithUserAgent(UserAgent()),
|
||||
}
|
||||
if auth != nil {
|
||||
opts = append(opts, remote.WithAuth(staticAuth{auth}))
|
||||
@@ -223,6 +224,7 @@ func GetImageDigest(targetImage, targetPlatform string, auth *registrytypes.Auth
|
||||
opts := []remote.Option{
|
||||
remote.WithTransport(tr),
|
||||
remote.WithPlatform(*platform),
|
||||
remote.WithUserAgent(UserAgent()),
|
||||
}
|
||||
if auth != nil {
|
||||
opts = append(opts, remote.WithAuth(staticAuth{auth}))
|
||||
|
||||
@@ -47,6 +47,7 @@ func OllamaModelManifest(image string) (*Manifest, error) {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json")
|
||||
req.Header.Set("User-Agent", UserAgent())
|
||||
client := httpclient.New(httpclient.WithFollowRedirects())
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
||||
19
pkg/oci/useragent.go
Normal file
19
pkg/oci/useragent.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mudler/LocalAI/internal"
|
||||
)
|
||||
|
||||
// UserAgent returns the User-Agent string LocalAI sends on outbound registry
|
||||
// requests (OCI registries and Ollama). It identifies the client as LocalAI
|
||||
// and, when the binary was built with a version stamp, appends it so registries
|
||||
// can attribute client-side usage to LocalAI rather than to the generic
|
||||
// User-Agent of the underlying transport library.
|
||||
func UserAgent() string {
|
||||
if internal.Version == "" {
|
||||
return "LocalAI"
|
||||
}
|
||||
return fmt.Sprintf("LocalAI/%s", internal.Version)
|
||||
}
|
||||
32
pkg/oci/useragent_test.go
Normal file
32
pkg/oci/useragent_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package oci_test
|
||||
|
||||
import (
|
||||
"github.com/mudler/LocalAI/internal"
|
||||
. "github.com/mudler/LocalAI/pkg/oci"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("OCI", func() {
|
||||
Context("UserAgent", func() {
|
||||
var savedVersion string
|
||||
|
||||
BeforeEach(func() {
|
||||
savedVersion = internal.Version
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
internal.Version = savedVersion
|
||||
})
|
||||
|
||||
It("identifies as LocalAI when no version is stamped", func() {
|
||||
internal.Version = ""
|
||||
Expect(UserAgent()).To(Equal("LocalAI"))
|
||||
})
|
||||
|
||||
It("appends the build version when one is stamped", func() {
|
||||
internal.Version = "v3.2.1"
|
||||
Expect(UserAgent()).To(Equal("LocalAI/v3.2.1"))
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user