mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-01-20 19:58:03 -05:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8706fbba3b | ||
|
|
b169576cd8 | ||
|
|
da34f5823a | ||
|
|
14358a1c1b | ||
|
|
ce74e881be | ||
|
|
a4399000cf | ||
|
|
6f804d7e46 | ||
|
|
8c015b09a1 | ||
|
|
03108cc69d | ||
|
|
556e098fc1 |
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## 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.383"
|
||||
|
||||
Binary file not shown.
@@ -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.383"
|
||||
|
||||
Reference in New Issue
Block a user