mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-01-19 19:29:02 -05:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb407ccfed | ||
|
|
c9d4c19ef8 | ||
|
|
f4e7489d42 | ||
|
|
7012acd12a | ||
|
|
387610bcf8 | ||
|
|
9e1ee4d48e | ||
|
|
8706fbba3b | ||
|
|
b169576cd8 | ||
|
|
da34f5823a | ||
|
|
14358a1c1b | ||
|
|
ce74e881be | ||
|
|
a4399000cf | ||
|
|
6f804d7e46 | ||
|
|
8c015b09a1 | ||
|
|
03108cc69d | ||
|
|
556e098fc1 |
20
CHANGELOG.md
20
CHANGELOG.md
@@ -1,5 +1,25 @@
|
||||
# Changelog
|
||||
|
||||
## v1.4.384 (2026-01-19)
|
||||
|
||||
### PR [#1944](https://github.com/danielmiessler/Fabric/pull/1944) by [ksylvan](https://github.com/ksylvan): Add Infermatic AI Provider Support
|
||||
|
||||
- Add Infermatic provider to ProviderMap as part of Phase 1 implementation for issue #1033
|
||||
- Add test coverage for the Infermatic AI provider in TestCreateClient to verify provider exists and creates valid client
|
||||
- Replace go-git status API with native `git status --porcelain` command to fix worktree compatibility issues
|
||||
- Simplify `IsWorkingDirectoryClean` and `GetStatusDetails` functions to use CLI output parsing instead of go-git library
|
||||
- Use native `git rev-parse HEAD` to get commit hash after commit and remove unused imports from walker.go
|
||||
|
||||
## v1.4.383 (2026-01-18)
|
||||
|
||||
### PR [#1943](https://github.com/danielmiessler/Fabric/pull/1943) by [ksylvan](https://github.com/ksylvan): fix: Ollama server now respects the default context window
|
||||
|
||||
- Fix: Ollama server now respects the default context window instead of using hardcoded 2048 tokens
|
||||
- Add parseOllamaNumCtx() function with type-safe extraction supporting 6 numeric types and platform-aware integer overflow protection
|
||||
- Extract num_ctx from client request options and add ModelContextLength field to ChatRequest struct
|
||||
- Implement DoS protection via 1,000,000 token maximum limit with sanitized error messages
|
||||
- Add comprehensive unit tests for parseOllamaNumCtx function covering edge cases including overflow and invalid types
|
||||
|
||||
## v1.4.382 (2026-01-17)
|
||||
|
||||
### PR [#1941](https://github.com/danielmiessler/Fabric/pull/1941) by [ksylvan](https://github.com/ksylvan): Add `greybeard_secure_prompt_engineer` to metadata, also remove duplicate json data file
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
package main
|
||||
|
||||
var version = "v1.4.382"
|
||||
var version = "v1.4.384"
|
||||
|
||||
Binary file not shown.
@@ -2,9 +2,7 @@ package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -425,64 +423,49 @@ func (w *Walker) Repository() *git.Repository {
|
||||
}
|
||||
|
||||
// IsWorkingDirectoryClean checks if the working directory has any uncommitted changes
|
||||
// Uses native git CLI instead of go-git to properly handle worktree scenarios
|
||||
func (w *Walker) IsWorkingDirectoryClean() (bool, error) {
|
||||
worktree, err := w.repo.Worktree()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get worktree: %w", err)
|
||||
}
|
||||
|
||||
status, err := worktree.Status()
|
||||
worktreePath := worktree.Filesystem.Root()
|
||||
|
||||
// Use native git status --porcelain to avoid go-git worktree issues
|
||||
// go-git's status API has known bugs with linked worktrees
|
||||
cmd := exec.Command("git", "status", "--porcelain")
|
||||
cmd.Dir = worktreePath
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get git status: %w", err)
|
||||
}
|
||||
|
||||
worktreePath := worktree.Filesystem.Root()
|
||||
|
||||
// In worktrees, files staged in the main repo may appear in status but not exist in the worktree
|
||||
// We need to check both the working directory status AND filesystem existence
|
||||
for file, fileStatus := range status {
|
||||
// Check if there are any changes in the working directory
|
||||
if fileStatus.Worktree != git.Unmodified && fileStatus.Worktree != git.Untracked {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// For staged files (Added, Modified in index), verify they exist in this worktree's filesystem
|
||||
// This handles the worktree case where the main repo has staged files that don't exist here
|
||||
if fileStatus.Staging != git.Unmodified && fileStatus.Staging != git.Untracked {
|
||||
filePath := filepath.Join(worktreePath, file)
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
// File is staged but doesn't exist in this worktree - ignore it
|
||||
continue
|
||||
}
|
||||
// File is staged AND exists in this worktree - not clean
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
// If output is empty, working directory is clean
|
||||
return len(strings.TrimSpace(string(output))) == 0, nil
|
||||
}
|
||||
|
||||
// GetStatusDetails returns a detailed status of the working directory
|
||||
// Uses native git CLI instead of go-git to properly handle worktree scenarios
|
||||
func (w *Walker) GetStatusDetails() (string, error) {
|
||||
worktree, err := w.repo.Worktree()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get worktree: %w", err)
|
||||
}
|
||||
|
||||
status, err := worktree.Status()
|
||||
worktreePath := worktree.Filesystem.Root()
|
||||
|
||||
// Use native git status --porcelain to avoid go-git worktree issues
|
||||
cmd := exec.Command("git", "status", "--porcelain")
|
||||
cmd.Dir = worktreePath
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get git status: %w", err)
|
||||
}
|
||||
|
||||
var details strings.Builder
|
||||
for file, fileStatus := range status {
|
||||
// Only include files with actual working directory changes
|
||||
if fileStatus.Worktree != git.Unmodified && fileStatus.Worktree != git.Untracked {
|
||||
details.WriteString(fmt.Sprintf(" %c%c %s\n", fileStatus.Staging, fileStatus.Worktree, file))
|
||||
}
|
||||
}
|
||||
|
||||
return details.String(), nil
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
// AddFile adds a file to the git index
|
||||
@@ -526,13 +509,17 @@ func (w *Walker) CommitChanges(message string) (plumbing.Hash, error) {
|
||||
return plumbing.ZeroHash, fmt.Errorf("failed to commit: %w (output: %s)", err, string(output))
|
||||
}
|
||||
|
||||
// Get the commit hash from HEAD
|
||||
ref, err := w.repo.Head()
|
||||
// Get the commit hash from HEAD using native git to avoid go-git worktree issues
|
||||
hashCmd := exec.Command("git", "rev-parse", "HEAD")
|
||||
hashCmd.Dir = worktreePath
|
||||
|
||||
hashOutput, err := hashCmd.Output()
|
||||
if err != nil {
|
||||
return plumbing.ZeroHash, fmt.Errorf("failed to get HEAD after commit: %w", err)
|
||||
}
|
||||
|
||||
return ref.Hash(), nil
|
||||
hashStr := strings.TrimSpace(string(hashOutput))
|
||||
return plumbing.NewHash(hashStr), nil
|
||||
}
|
||||
|
||||
// PushToRemote pushes the current branch to the remote repository
|
||||
|
||||
@@ -145,6 +145,11 @@ var ProviderMap = map[string]ProviderConfig{
|
||||
ModelsURL: "https://models.github.ai/catalog", // FetchModelsDirectly will append /models
|
||||
ImplementsResponses: false,
|
||||
},
|
||||
"Infermatic": {
|
||||
Name: "Infermatic",
|
||||
BaseURL: "https://api.totalgpt.ai/v1",
|
||||
ImplementsResponses: false,
|
||||
},
|
||||
"GrokAI": {
|
||||
Name: "GrokAI",
|
||||
BaseURL: "https://api.x.ai/v1",
|
||||
|
||||
@@ -30,6 +30,11 @@ func TestCreateClient(t *testing.T) {
|
||||
provider: "Abacus",
|
||||
exists: true,
|
||||
},
|
||||
{
|
||||
name: "Existing provider - Infermatic",
|
||||
provider: "Infermatic",
|
||||
exists: true,
|
||||
},
|
||||
{
|
||||
name: "Existing provider - MiniMax",
|
||||
provider: "MiniMax",
|
||||
|
||||
@@ -35,7 +35,8 @@ type PromptRequest struct {
|
||||
|
||||
type ChatRequest struct {
|
||||
Prompts []PromptRequest `json:"prompts"`
|
||||
Language string `json:"language"` // Add Language field to bind from request
|
||||
Language string `json:"language"`
|
||||
ModelContextLength int `json:"modelContextLength,omitempty"` // Context window size
|
||||
domain.ChatOptions // Embed the ChatOptions from common package
|
||||
}
|
||||
|
||||
@@ -118,7 +119,7 @@ func (h *ChatHandler) HandleChat(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
chatter, err := h.registry.GetChatter(p.Model, 2048, p.Vendor, "", true, false)
|
||||
chatter, err := h.registry.GetChatter(p.Model, request.ModelContextLength, p.Vendor, "", true, false)
|
||||
if err != nil {
|
||||
log.Printf("Error creating chatter: %v", err)
|
||||
streamChan <- domain.StreamUpdate{Type: domain.StreamTypeError, Content: fmt.Sprintf("Error: %v", err)}
|
||||
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -79,6 +81,111 @@ type FabricResponseFormat struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// parseOllamaNumCtx extracts and validates the num_ctx parameter from Ollama request options.
|
||||
// Returns:
|
||||
// - (0, nil) if num_ctx is not present or is null
|
||||
// - (n, nil) if num_ctx is a valid positive integer
|
||||
// - (0, error) if num_ctx is present but invalid
|
||||
func parseOllamaNumCtx(options map[string]any) (int, error) {
|
||||
if options == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
val, exists := options["num_ctx"]
|
||||
if !exists {
|
||||
return 0, nil // Not provided, caller should use default
|
||||
}
|
||||
|
||||
if val == nil {
|
||||
return 0, nil // Explicit null, treat as not provided
|
||||
}
|
||||
|
||||
var contextLength int
|
||||
|
||||
// Platform-specific max int value for overflow checks
|
||||
const maxInt = int64(^uint(0) >> 1)
|
||||
|
||||
switch v := val.(type) {
|
||||
case float64:
|
||||
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
return 0, fmt.Errorf("num_ctx must be a finite number")
|
||||
}
|
||||
if math.Trunc(v) != v {
|
||||
return 0, fmt.Errorf("num_ctx must be an integer, got float with fractional part")
|
||||
}
|
||||
// Check for overflow on 32-bit systems (negative values handled by validation at line 166)
|
||||
if v > float64(maxInt) {
|
||||
return 0, fmt.Errorf("num_ctx value out of range")
|
||||
}
|
||||
contextLength = int(v)
|
||||
|
||||
case float32:
|
||||
f64 := float64(v)
|
||||
if math.IsNaN(f64) || math.IsInf(f64, 0) {
|
||||
return 0, fmt.Errorf("num_ctx must be a finite number")
|
||||
}
|
||||
if math.Trunc(f64) != f64 {
|
||||
return 0, fmt.Errorf("num_ctx must be an integer, got float with fractional part")
|
||||
}
|
||||
// Check for overflow on 32-bit systems (negative values handled by validation at line 177)
|
||||
if f64 > float64(maxInt) {
|
||||
return 0, fmt.Errorf("num_ctx value out of range")
|
||||
}
|
||||
contextLength = int(v)
|
||||
|
||||
case int:
|
||||
contextLength = v
|
||||
|
||||
case int64:
|
||||
if v < 0 {
|
||||
return 0, fmt.Errorf("num_ctx must be positive, got: %d", v)
|
||||
}
|
||||
if v > maxInt {
|
||||
return 0, fmt.Errorf("num_ctx value too large: %d", v)
|
||||
}
|
||||
contextLength = int(v)
|
||||
|
||||
case json.Number:
|
||||
i64, err := v.Int64()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("num_ctx must be a valid number")
|
||||
}
|
||||
if i64 < 0 {
|
||||
return 0, fmt.Errorf("num_ctx must be positive, got: %d", i64)
|
||||
}
|
||||
if i64 > maxInt {
|
||||
return 0, fmt.Errorf("num_ctx value too large: %d", i64)
|
||||
}
|
||||
contextLength = int(i64)
|
||||
|
||||
case string:
|
||||
parsed, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
// Truncate long strings in error messages to avoid logging excessively large input
|
||||
errVal := v
|
||||
if len(v) > 50 {
|
||||
errVal = v[:50] + "..."
|
||||
}
|
||||
return 0, fmt.Errorf("num_ctx must be a valid number, got: %s", errVal)
|
||||
}
|
||||
contextLength = parsed
|
||||
|
||||
default:
|
||||
return 0, fmt.Errorf("num_ctx must be a number, got invalid type")
|
||||
}
|
||||
|
||||
if contextLength <= 0 {
|
||||
return 0, fmt.Errorf("num_ctx must be positive, got: %d", contextLength)
|
||||
}
|
||||
|
||||
const maxContextLength = 1000000
|
||||
if contextLength > maxContextLength {
|
||||
return 0, fmt.Errorf("num_ctx exceeds maximum allowed value of %d", maxContextLength)
|
||||
}
|
||||
|
||||
return contextLength, nil
|
||||
}
|
||||
|
||||
func ServeOllama(registry *core.PluginRegistry, address string, version string) (err error) {
|
||||
r := gin.New()
|
||||
|
||||
@@ -161,6 +268,15 @@ func (f APIConvert) ollamaChat(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "testing endpoint"})
|
||||
return
|
||||
}
|
||||
|
||||
// Extract and validate num_ctx from options
|
||||
numCtx, err := parseOllamaNumCtx(prompt.Options)
|
||||
if err != nil {
|
||||
log.Printf("Invalid num_ctx in request: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
var chat ChatRequest
|
||||
|
||||
@@ -210,6 +326,10 @@ func (f APIConvert) ollamaChat(c *gin.Context) {
|
||||
Variables: variables,
|
||||
}}
|
||||
}
|
||||
|
||||
// Set context length from parsed num_ctx
|
||||
chat.ModelContextLength = numCtx
|
||||
|
||||
fabricChatReq, err := json.Marshal(chat)
|
||||
if err != nil {
|
||||
log.Printf("Error marshalling body: %v", err)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package restapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -98,3 +101,263 @@ func TestBuildFabricChatURL(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOllamaNumCtx(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options map[string]any
|
||||
want int
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
// --- Valid inputs ---
|
||||
{
|
||||
name: "nil options",
|
||||
options: nil,
|
||||
want: 0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty options",
|
||||
options: map[string]any{},
|
||||
want: 0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "num_ctx not present",
|
||||
options: map[string]any{"other_key": 123},
|
||||
want: 0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "num_ctx is null",
|
||||
options: map[string]any{"num_ctx": nil},
|
||||
want: 0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid int",
|
||||
options: map[string]any{"num_ctx": 4096},
|
||||
want: 4096,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid float64 (whole number)",
|
||||
options: map[string]any{"num_ctx": float64(8192)},
|
||||
want: 8192,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid float32 (whole number)",
|
||||
options: map[string]any{"num_ctx": float32(2048)},
|
||||
want: 2048,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid json.Number",
|
||||
options: map[string]any{"num_ctx": json.Number("16384")},
|
||||
want: 16384,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid string",
|
||||
options: map[string]any{"num_ctx": "32768"},
|
||||
want: 32768,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid int64",
|
||||
options: map[string]any{"num_ctx": int64(65536)},
|
||||
want: 65536,
|
||||
wantErr: false,
|
||||
},
|
||||
// --- Invalid inputs ---
|
||||
{
|
||||
name: "float64 with fractional part",
|
||||
options: map[string]any{"num_ctx": 4096.5},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be an integer, got float with fractional part",
|
||||
},
|
||||
{
|
||||
name: "float32 with fractional part",
|
||||
options: map[string]any{"num_ctx": float32(2048.75)},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be an integer, got float with fractional part",
|
||||
},
|
||||
{
|
||||
name: "negative int",
|
||||
options: map[string]any{"num_ctx": -100},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be positive",
|
||||
},
|
||||
{
|
||||
name: "zero int",
|
||||
options: map[string]any{"num_ctx": 0},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be positive",
|
||||
},
|
||||
{
|
||||
name: "negative float64",
|
||||
options: map[string]any{"num_ctx": float64(-500)},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be positive",
|
||||
},
|
||||
{
|
||||
name: "negative float32",
|
||||
options: map[string]any{"num_ctx": float32(-250)},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be positive",
|
||||
},
|
||||
{
|
||||
name: "non-numeric string",
|
||||
options: map[string]any{"num_ctx": "not-a-number"},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a valid number",
|
||||
},
|
||||
{
|
||||
name: "invalid json.Number",
|
||||
options: map[string]any{"num_ctx": json.Number("invalid")},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a valid number",
|
||||
},
|
||||
{
|
||||
name: "exceeds maximum allowed value",
|
||||
options: map[string]any{"num_ctx": 2000000},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx exceeds maximum allowed value",
|
||||
},
|
||||
{
|
||||
name: "unsupported type (bool)",
|
||||
options: map[string]any{"num_ctx": true},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a number, got invalid type",
|
||||
},
|
||||
{
|
||||
name: "unsupported type (slice)",
|
||||
options: map[string]any{"num_ctx": []int{1, 2, 3}},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a number, got invalid type",
|
||||
},
|
||||
// --- Edge cases ---
|
||||
{
|
||||
name: "minimum valid value",
|
||||
options: map[string]any{"num_ctx": 1},
|
||||
want: 1,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "maximum allowed value",
|
||||
options: map[string]any{"num_ctx": 1000000},
|
||||
want: 1000000,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "very large float64 (overflow)",
|
||||
options: map[string]any{"num_ctx": float64(math.MaxFloat64)},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx value out of range",
|
||||
},
|
||||
{
|
||||
name: "large int64 exceeding maxInt on 32-bit",
|
||||
options: map[string]any{"num_ctx": int64(1 << 40)},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx", // either "too large" or "exceeds maximum"
|
||||
},
|
||||
{
|
||||
name: "long string gets truncated in error",
|
||||
options: map[string]any{"num_ctx": "this-is-a-very-long-string-that-should-be-truncated-in-the-error-message"},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a valid number",
|
||||
},
|
||||
// --- Special float values ---
|
||||
{
|
||||
name: "float64 NaN",
|
||||
options: map[string]any{"num_ctx": math.NaN()},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a finite number",
|
||||
},
|
||||
{
|
||||
name: "float64 positive infinity",
|
||||
options: map[string]any{"num_ctx": math.Inf(1)},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a finite number",
|
||||
},
|
||||
{
|
||||
name: "float64 negative infinity",
|
||||
options: map[string]any{"num_ctx": math.Inf(-1)},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a finite number",
|
||||
},
|
||||
{
|
||||
name: "float32 NaN",
|
||||
options: map[string]any{"num_ctx": float32(math.NaN())},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a finite number",
|
||||
},
|
||||
{
|
||||
name: "float32 positive infinity",
|
||||
options: map[string]any{"num_ctx": float32(math.Inf(1))},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a finite number",
|
||||
},
|
||||
{
|
||||
name: "float32 negative infinity",
|
||||
options: map[string]any{"num_ctx": float32(math.Inf(-1))},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be a finite number",
|
||||
},
|
||||
// --- Negative int64 (32-bit wraparound prevention) ---
|
||||
{
|
||||
name: "negative int64",
|
||||
options: map[string]any{"num_ctx": int64(-1000)},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be positive",
|
||||
},
|
||||
{
|
||||
name: "negative json.Number",
|
||||
options: map[string]any{"num_ctx": json.Number("-500")},
|
||||
want: 0,
|
||||
wantErr: true,
|
||||
errMsg: "num_ctx must be positive",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseOllamaNumCtx(tt.options)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseOllamaNumCtx() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil && tt.errMsg != "" {
|
||||
if !strings.Contains(err.Error(), tt.errMsg) {
|
||||
t.Errorf("parseOllamaNumCtx() error message = %q, want to contain %q", err.Error(), tt.errMsg)
|
||||
}
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("parseOllamaNumCtx() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
"1.4.382"
|
||||
"1.4.384"
|
||||
|
||||
Reference in New Issue
Block a user