Keep checkpoints aligned with PromptKit profiles
This commit is contained in:
14
internal/framework/llm/checkpoint_fingerprint.go
Normal file
14
internal/framework/llm/checkpoint_fingerprint.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package llm
|
||||
|
||||
// CheckpointFingerprint is a stable, non-secret semantic identity contributed
|
||||
// by the LLM runtime before pipeline execution.
|
||||
type CheckpointFingerprint struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
// CheckpointFingerprintProvider exposes LLM-runtime identities that must
|
||||
// participate in checkpoint composition.
|
||||
type CheckpointFingerprintProvider interface {
|
||||
LLMCheckpointFingerprints() ([]CheckpointFingerprint, error)
|
||||
}
|
||||
@@ -29,8 +29,10 @@ type PromptKitClientConfig struct {
|
||||
}
|
||||
|
||||
type PromptKitClient struct {
|
||||
engine *promptkit.Engine
|
||||
recorder *LLMProfileRecorder
|
||||
engine *promptkit.Engine
|
||||
recorder *LLMProfileRecorder
|
||||
profileDir string
|
||||
profileFile string
|
||||
}
|
||||
|
||||
type LLMProfileRecorder struct {
|
||||
@@ -70,8 +72,10 @@ func NewPromptKitClient(cfg PromptKitClientConfig) (*PromptKitClient, error) {
|
||||
recorder = NewLLMProfileRecorder()
|
||||
}
|
||||
return &PromptKitClient{
|
||||
engine: engine,
|
||||
recorder: recorder,
|
||||
engine: engine,
|
||||
recorder: recorder,
|
||||
profileDir: strings.TrimSpace(cfg.ProfileDir),
|
||||
profileFile: strings.TrimSpace(cfg.ProfileFile),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -261,6 +265,17 @@ func (c *PromptKitClient) LLMProfileManifests() []artifacts.LLMProfileManifest {
|
||||
return c.recorder.Manifests()
|
||||
}
|
||||
|
||||
func (c *PromptKitClient) LLMCheckpointFingerprints() ([]CheckpointFingerprint, error) {
|
||||
if c == nil {
|
||||
return nil, nil
|
||||
}
|
||||
fingerprint, err := promptKitProfileFingerprint(c.profileDir, c.profileFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []CheckpointFingerprint{fingerprint}, nil
|
||||
}
|
||||
|
||||
func NewLLMProfileRecorder() *LLMProfileRecorder {
|
||||
return &LLMProfileRecorder{profiles: map[string]artifacts.LLMProfileManifest{}}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -119,6 +121,63 @@ func TestNewPromptKitClientReportsAssetAndEngineConstructionFailures(t *testing.
|
||||
})
|
||||
}
|
||||
|
||||
func TestPromptKitClientCheckpointFingerprintTracksProfileSource(t *testing.T) {
|
||||
profilePath := filepath.Join(t.TempDir(), "profiles.yml")
|
||||
writeProfile := func(model string) {
|
||||
t.Helper()
|
||||
content := "id: checkpoint-profile\nendpoint: http://promptkit.test/v1\nmodel: " + model + "\n"
|
||||
if err := os.WriteFile(profilePath, []byte(content), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
fingerprintFor := func() CheckpointFingerprint {
|
||||
t.Helper()
|
||||
client, err := NewPromptKitClient(PromptKitClientConfig{
|
||||
Assets: newTestPromptKitAssets(t),
|
||||
ProfileFile: profilePath,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
values, err := client.LLMCheckpointFingerprints()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(values) != 1 || values[0].Name != promptKitProfileFingerprintName {
|
||||
t.Fatalf("checkpoint fingerprints = %#v, want one profile-source identity", values)
|
||||
}
|
||||
return values[0]
|
||||
}
|
||||
|
||||
writeProfile("model-one")
|
||||
first := fingerprintFor()
|
||||
writeProfile("model-two")
|
||||
second := fingerprintFor()
|
||||
if first == second {
|
||||
t.Fatalf("profile-source fingerprint = %#v for both profile models", first)
|
||||
}
|
||||
if strings.Contains(first.Value, profilePath) || strings.Contains(first.Value, "model-one") {
|
||||
t.Fatalf("profile-source fingerprint exposes source details: %#v", first)
|
||||
}
|
||||
|
||||
client, err := NewPromptKitClient(PromptKitClientConfig{Assets: newTestPromptKitAssets(t)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
copy, err := client.LLMCheckpointFingerprints()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
copy[0].Value = "mutated"
|
||||
fresh, err := client.LLMCheckpointFingerprints()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fresh[0].Value == "mutated" {
|
||||
t.Fatal("LLMCheckpointFingerprints exposed mutable backing storage")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptKitClientUsesPromptDefaultProfileWhenRequestProfileEmpty(t *testing.T) {
|
||||
fake := &fakePromptKitLLM{content: `{"ok":true}`}
|
||||
client := newTestPromptKitClient(t, fake)
|
||||
|
||||
82
internal/framework/llm/promptkit_profile_fingerprint.go
Normal file
82
internal/framework/llm/promptkit_profile_fingerprint.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
promptKitProfileFingerprintName = "promptkit_profile_source"
|
||||
// The built-in profile catalog is compiled into this pinned PromptKit
|
||||
// release. Update this identity when the dependency is upgraded.
|
||||
promptKitBuiltinProfileCatalogID = "promptkit:v0.1.0:builtin-profiles"
|
||||
)
|
||||
|
||||
func promptKitProfileFingerprint(profileDir, profileFile string) (CheckpointFingerprint, error) {
|
||||
hasher := sha256.New()
|
||||
writeFingerprintPart(hasher, []byte(promptKitBuiltinProfileCatalogID))
|
||||
|
||||
switch {
|
||||
case strings.TrimSpace(profileFile) != "":
|
||||
data, err := os.ReadFile(strings.TrimSpace(profileFile))
|
||||
if err != nil {
|
||||
return CheckpointFingerprint{}, fmt.Errorf("read PromptKit profile file for checkpoint identity: %w", err)
|
||||
}
|
||||
writeFingerprintPart(hasher, data)
|
||||
case strings.TrimSpace(profileDir) != "":
|
||||
digests, err := promptKitProfileFileDigests(strings.TrimSpace(profileDir))
|
||||
if err != nil {
|
||||
return CheckpointFingerprint{}, err
|
||||
}
|
||||
for _, digest := range digests {
|
||||
writeFingerprintPart(hasher, digest)
|
||||
}
|
||||
}
|
||||
|
||||
return CheckpointFingerprint{
|
||||
Name: promptKitProfileFingerprintName,
|
||||
Value: "sha256:" + hex.EncodeToString(hasher.Sum(nil)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func promptKitProfileFileDigests(root string) ([][]byte, error) {
|
||||
var digests [][]byte
|
||||
err := filepath.WalkDir(root, func(name string, entry fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
extension := filepath.Ext(entry.Name())
|
||||
if extension != ".yaml" && extension != ".yml" {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sum := sha256.Sum256(data)
|
||||
digests = append(digests, append([]byte(nil), sum[:]...))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read PromptKit profile directory for checkpoint identity: %w", err)
|
||||
}
|
||||
sort.Slice(digests, func(i, j int) bool {
|
||||
return string(digests[i]) < string(digests[j])
|
||||
})
|
||||
return digests, nil
|
||||
}
|
||||
|
||||
func writeFingerprintPart(hasher interface{ Write([]byte) (int, error) }, value []byte) {
|
||||
length := []byte(fmt.Sprintf("%d:", len(value)))
|
||||
_, _ = hasher.Write(length)
|
||||
_, _ = hasher.Write(value)
|
||||
}
|
||||
@@ -53,3 +53,14 @@ func (c *scheduledClient) LLMProfileManifests() []artifacts.LLMProfileManifest {
|
||||
}
|
||||
return provider.LLMProfileManifests()
|
||||
}
|
||||
|
||||
func (c *scheduledClient) LLMCheckpointFingerprints() ([]CheckpointFingerprint, error) {
|
||||
if c == nil || c.client == nil {
|
||||
return nil, nil
|
||||
}
|
||||
provider, ok := c.client.(CheckpointFingerprintProvider)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return provider.LLMCheckpointFingerprints()
|
||||
}
|
||||
|
||||
@@ -75,6 +75,28 @@ func TestScheduledClientPropagatesSchedulerError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestScheduledClientPreservesCheckpointFingerprints(t *testing.T) {
|
||||
scheduler, err := NewScheduler(1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
inner := &fingerprintedStructuredClient{
|
||||
fingerprints: []CheckpointFingerprint{{Name: "profile_source", Value: "sha256:one"}},
|
||||
}
|
||||
client := NewScheduledClient(inner, scheduler)
|
||||
provider, ok := client.(CheckpointFingerprintProvider)
|
||||
if !ok {
|
||||
t.Fatalf("scheduled client %T does not preserve checkpoint fingerprints", client)
|
||||
}
|
||||
got, err := provider.LLMCheckpointFingerprints()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != 1 || got[0] != inner.fingerprints[0] {
|
||||
t.Fatalf("checkpoint fingerprints = %#v, want %#v", got, inner.fingerprints)
|
||||
}
|
||||
}
|
||||
|
||||
type blockingStructuredClient struct {
|
||||
release chan struct{}
|
||||
inFlight int32
|
||||
@@ -110,6 +132,15 @@ type errorStructuredClient struct {
|
||||
err error
|
||||
}
|
||||
|
||||
type fingerprintedStructuredClient struct {
|
||||
errorStructuredClient
|
||||
fingerprints []CheckpointFingerprint
|
||||
}
|
||||
|
||||
func (c *fingerprintedStructuredClient) LLMCheckpointFingerprints() ([]CheckpointFingerprint, error) {
|
||||
return append([]CheckpointFingerprint(nil), c.fingerprints...), nil
|
||||
}
|
||||
|
||||
func (c *errorStructuredClient) CompleteStructured(ctx context.Context, req contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) {
|
||||
return contracts.StructuredCompletionResponse{}, c.err
|
||||
}
|
||||
|
||||
@@ -11,15 +11,15 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ModulePromptFile maps a module-owned embedded prompt file into the
|
||||
// PromptKit-visible module prompt directory.
|
||||
// ModulePromptFile maps a module-owned embedded prompt file into the registered
|
||||
// module prompt directory.
|
||||
type ModulePromptFile struct {
|
||||
Name string
|
||||
Path string
|
||||
}
|
||||
|
||||
// SharedPromptFile maps a caller-owned shared prompt file into a module's
|
||||
// PromptKit-visible sharedassets prompt subdirectory.
|
||||
// registered sharedassets prompt subdirectory.
|
||||
type SharedPromptFile struct {
|
||||
Name string
|
||||
FS fs.FS
|
||||
|
||||
Reference in New Issue
Block a user