feat(openai_compat): add strict_compat option to strip non-standard fields
Some OpenAI-compatible providers (e.g. OpenRouter routing to strict backends) reject non-standard fields in the request body such as reasoning_content in messages and extra_content / thought_signature in tool calls. Add a per-model strict_compat: true config option that strips these fields before serialization. Implementation: - Add StrictCompat bool to config.ModelConfig - Add WithStrictCompat option to openai_compat.Provider - Refactor HTTPProvider constructors into a single NewHTTPProviderWithOptions using variadic openai_compat.Option, eliminating the growing list of named constructors - Thread StrictCompat through CreateProviderFromConfig via composed options Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2eb78cec88
commit
0e3a0d8277
5 changed files with 189 additions and 58 deletions
|
|
@ -608,6 +608,7 @@ type ModelConfig struct {
|
||||||
RPM int `json:"rpm,omitempty"` // Requests per minute limit
|
RPM int `json:"rpm,omitempty"` // Requests per minute limit
|
||||||
MaxTokensField string `json:"max_tokens_field,omitempty"` // Field name for max tokens (e.g., "max_completion_tokens")
|
MaxTokensField string `json:"max_tokens_field,omitempty"` // Field name for max tokens (e.g., "max_completion_tokens")
|
||||||
RequestTimeout int `json:"request_timeout,omitempty"`
|
RequestTimeout int `json:"request_timeout,omitempty"`
|
||||||
|
StrictCompat bool `json:"strict_compat,omitempty"` // Strip non-standard fields for strict OpenAI-compatible endpoints
|
||||||
ThinkingLevel string `json:"thinking_level,omitempty"` // Extended thinking: off|low|medium|high|xhigh|adaptive
|
ThinkingLevel string `json:"thinking_level,omitempty"` // Extended thinking: off|low|medium|high|xhigh|adaptive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,12 @@ package providers
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
anthropicmessages "github.com/sipeed/picoclaw/pkg/providers/anthropic_messages"
|
anthropicmessages "github.com/sipeed/picoclaw/pkg/providers/anthropic_messages"
|
||||||
"github.com/sipeed/picoclaw/pkg/providers/azure"
|
"github.com/sipeed/picoclaw/pkg/providers/azure"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/providers/openai_compat"
|
||||||
)
|
)
|
||||||
|
|
||||||
// createClaudeAuthProvider creates a Claude provider using OAuth credentials from auth store.
|
// createClaudeAuthProvider creates a Claude provider using OAuth credentials from auth store.
|
||||||
|
|
@ -87,13 +89,12 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
||||||
if apiBase == "" {
|
if apiBase == "" {
|
||||||
apiBase = getDefaultAPIBase(protocol)
|
apiBase = getDefaultAPIBase(protocol)
|
||||||
}
|
}
|
||||||
return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
|
opts := []openai_compat.Option{
|
||||||
cfg.APIKey,
|
openai_compat.WithMaxTokensField(cfg.MaxTokensField),
|
||||||
apiBase,
|
openai_compat.WithRequestTimeout(time.Duration(cfg.RequestTimeout) * time.Second),
|
||||||
cfg.Proxy,
|
openai_compat.WithStrictCompat(cfg.StrictCompat),
|
||||||
cfg.MaxTokensField,
|
}
|
||||||
cfg.RequestTimeout,
|
return NewHTTPProviderWithOptions(cfg.APIKey, apiBase, cfg.Proxy, opts...), modelID, nil
|
||||||
), modelID, nil
|
|
||||||
|
|
||||||
case "azure", "azure-openai":
|
case "azure", "azure-openai":
|
||||||
// Azure OpenAI uses deployment-based URLs, api-key header auth,
|
// Azure OpenAI uses deployment-based URLs, api-key header auth,
|
||||||
|
|
@ -125,13 +126,12 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
||||||
if apiBase == "" {
|
if apiBase == "" {
|
||||||
apiBase = getDefaultAPIBase(protocol)
|
apiBase = getDefaultAPIBase(protocol)
|
||||||
}
|
}
|
||||||
return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
|
opts := []openai_compat.Option{
|
||||||
cfg.APIKey,
|
openai_compat.WithMaxTokensField(cfg.MaxTokensField),
|
||||||
apiBase,
|
openai_compat.WithRequestTimeout(time.Duration(cfg.RequestTimeout) * time.Second),
|
||||||
cfg.Proxy,
|
openai_compat.WithStrictCompat(cfg.StrictCompat),
|
||||||
cfg.MaxTokensField,
|
}
|
||||||
cfg.RequestTimeout,
|
return NewHTTPProviderWithOptions(cfg.APIKey, apiBase, cfg.Proxy, opts...), modelID, nil
|
||||||
), modelID, nil
|
|
||||||
|
|
||||||
case "anthropic":
|
case "anthropic":
|
||||||
if cfg.AuthMethod == "oauth" || cfg.AuthMethod == "token" {
|
if cfg.AuthMethod == "oauth" || cfg.AuthMethod == "token" {
|
||||||
|
|
@ -150,13 +150,12 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
||||||
if cfg.APIKey == "" {
|
if cfg.APIKey == "" {
|
||||||
return nil, "", fmt.Errorf("api_key is required for anthropic protocol (model: %s)", cfg.Model)
|
return nil, "", fmt.Errorf("api_key is required for anthropic protocol (model: %s)", cfg.Model)
|
||||||
}
|
}
|
||||||
return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
|
opts := []openai_compat.Option{
|
||||||
cfg.APIKey,
|
openai_compat.WithMaxTokensField(cfg.MaxTokensField),
|
||||||
apiBase,
|
openai_compat.WithRequestTimeout(time.Duration(cfg.RequestTimeout) * time.Second),
|
||||||
cfg.Proxy,
|
openai_compat.WithStrictCompat(cfg.StrictCompat),
|
||||||
cfg.MaxTokensField,
|
}
|
||||||
cfg.RequestTimeout,
|
return NewHTTPProviderWithOptions(cfg.APIKey, apiBase, cfg.Proxy, opts...), modelID, nil
|
||||||
), modelID, nil
|
|
||||||
|
|
||||||
case "anthropic-messages":
|
case "anthropic-messages":
|
||||||
// Anthropic Messages API with native format (HTTP-based, no SDK)
|
// Anthropic Messages API with native format (HTTP-based, no SDK)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ package providers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/providers/openai_compat"
|
"github.com/sipeed/picoclaw/pkg/providers/openai_compat"
|
||||||
)
|
)
|
||||||
|
|
@ -17,28 +16,9 @@ type HTTPProvider struct {
|
||||||
delegate *openai_compat.Provider
|
delegate *openai_compat.Provider
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPProvider(apiKey, apiBase, proxy string) *HTTPProvider {
|
func NewHTTPProviderWithOptions(apiKey, apiBase, proxy string, opts ...openai_compat.Option) *HTTPProvider {
|
||||||
return &HTTPProvider{
|
return &HTTPProvider{
|
||||||
delegate: openai_compat.NewProvider(apiKey, apiBase, proxy),
|
delegate: openai_compat.NewProvider(apiKey, apiBase, proxy, opts...),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHTTPProviderWithMaxTokensField(apiKey, apiBase, proxy, maxTokensField string) *HTTPProvider {
|
|
||||||
return NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(apiKey, apiBase, proxy, maxTokensField, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHTTPProviderWithMaxTokensFieldAndRequestTimeout(
|
|
||||||
apiKey, apiBase, proxy, maxTokensField string,
|
|
||||||
requestTimeoutSeconds int,
|
|
||||||
) *HTTPProvider {
|
|
||||||
return &HTTPProvider{
|
|
||||||
delegate: openai_compat.NewProvider(
|
|
||||||
apiKey,
|
|
||||||
apiBase,
|
|
||||||
proxy,
|
|
||||||
openai_compat.WithMaxTokensField(maxTokensField),
|
|
||||||
openai_compat.WithRequestTimeout(time.Duration(requestTimeoutSeconds)*time.Second),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ type Provider struct {
|
||||||
apiKey string
|
apiKey string
|
||||||
apiBase string
|
apiBase string
|
||||||
maxTokensField string // Field name for max tokens (e.g., "max_completion_tokens" for o1/glm models)
|
maxTokensField string // Field name for max tokens (e.g., "max_completion_tokens" for o1/glm models)
|
||||||
|
strictCompat bool // Strip non-standard fields for strict OpenAI-compatible endpoints
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,6 +53,12 @@ func WithRequestTimeout(timeout time.Duration) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithStrictCompat(v bool) Option {
|
||||||
|
return func(p *Provider) {
|
||||||
|
p.strictCompat = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewProvider(apiKey, apiBase, proxy string, opts ...Option) *Provider {
|
func NewProvider(apiKey, apiBase, proxy string, opts ...Option) *Provider {
|
||||||
p := &Provider{
|
p := &Provider{
|
||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
|
|
@ -100,7 +107,7 @@ func (p *Provider) Chat(
|
||||||
|
|
||||||
requestBody := map[string]any{
|
requestBody := map[string]any{
|
||||||
"model": model,
|
"model": model,
|
||||||
"messages": common.SerializeMessages(messages),
|
"messages": serializeMessages(messages, p.strictCompat),
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tools) > 0 {
|
if len(tools) > 0 {
|
||||||
|
|
@ -202,15 +209,37 @@ func msgContent(content string, toolCalls []ToolCall) *string {
|
||||||
// - Strips SystemParts (unknown to third-party endpoints)
|
// - Strips SystemParts (unknown to third-party endpoints)
|
||||||
// - Converts messages with Media to multipart content format (text + image_url parts)
|
// - Converts messages with Media to multipart content format (text + image_url parts)
|
||||||
// - Preserves ToolCallID, ToolCalls, and ReasoningContent for all messages
|
// - Preserves ToolCallID, ToolCalls, and ReasoningContent for all messages
|
||||||
func serializeMessages(messages []Message) []any {
|
// - When strictCompat is true, strips non-standard fields (reasoning_content, extra_content,
|
||||||
|
// thought_signature) that some strict OpenAI-compatible providers reject
|
||||||
|
func serializeMessages(messages []Message, strictCompat bool) []any {
|
||||||
out := make([]any, 0, len(messages))
|
out := make([]any, 0, len(messages))
|
||||||
for _, m := range messages {
|
for _, m := range messages {
|
||||||
|
toolCalls := m.ToolCalls
|
||||||
|
reasoningContent := m.ReasoningContent
|
||||||
|
|
||||||
|
if strictCompat {
|
||||||
|
reasoningContent = ""
|
||||||
|
if len(toolCalls) > 0 {
|
||||||
|
sanitized := make([]ToolCall, len(toolCalls))
|
||||||
|
for i, tc := range toolCalls {
|
||||||
|
sanitized[i] = tc
|
||||||
|
sanitized[i].ExtraContent = nil
|
||||||
|
if tc.Function != nil {
|
||||||
|
fnCopy := *tc.Function
|
||||||
|
fnCopy.ThoughtSignature = ""
|
||||||
|
sanitized[i].Function = &fnCopy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toolCalls = sanitized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(m.Media) == 0 {
|
if len(m.Media) == 0 {
|
||||||
out = append(out, openaiMessage{
|
out = append(out, openaiMessage{
|
||||||
Role: m.Role,
|
Role: m.Role,
|
||||||
Content: msgContent(m.Content, m.ToolCalls),
|
Content: msgContent(m.Content, toolCalls),
|
||||||
ReasoningContent: m.ReasoningContent,
|
ReasoningContent: reasoningContent,
|
||||||
ToolCalls: m.ToolCalls,
|
ToolCalls: toolCalls,
|
||||||
ToolCallID: m.ToolCallID,
|
ToolCallID: m.ToolCallID,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
|
|
@ -242,11 +271,11 @@ func serializeMessages(messages []Message) []any {
|
||||||
if m.ToolCallID != "" {
|
if m.ToolCallID != "" {
|
||||||
msg["tool_call_id"] = m.ToolCallID
|
msg["tool_call_id"] = m.ToolCallID
|
||||||
}
|
}
|
||||||
if len(m.ToolCalls) > 0 {
|
if len(toolCalls) > 0 {
|
||||||
msg["tool_calls"] = m.ToolCalls
|
msg["tool_calls"] = toolCalls
|
||||||
}
|
}
|
||||||
if m.ReasoningContent != "" {
|
if reasoningContent != "" {
|
||||||
msg["reasoning_content"] = m.ReasoningContent
|
msg["reasoning_content"] = reasoningContent
|
||||||
}
|
}
|
||||||
out = append(out, msg)
|
out = append(out, msg)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -649,7 +649,7 @@ func TestSerializeMessages_PlainText(t *testing.T) {
|
||||||
{Role: "user", Content: "hello"},
|
{Role: "user", Content: "hello"},
|
||||||
{Role: "assistant", Content: "hi", ReasoningContent: "thinking..."},
|
{Role: "assistant", Content: "hi", ReasoningContent: "thinking..."},
|
||||||
}
|
}
|
||||||
result := common.SerializeMessages(messages)
|
result := serializeMessages(messages, false)
|
||||||
|
|
||||||
data, err := json.Marshal(result)
|
data, err := json.Marshal(result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -671,7 +671,7 @@ func TestSerializeMessages_WithMedia(t *testing.T) {
|
||||||
messages := []protocoltypes.Message{
|
messages := []protocoltypes.Message{
|
||||||
{Role: "user", Content: "describe this", Media: []string{"data:image/png;base64,abc123"}},
|
{Role: "user", Content: "describe this", Media: []string{"data:image/png;base64,abc123"}},
|
||||||
}
|
}
|
||||||
result := common.SerializeMessages(messages)
|
result := serializeMessages(messages, false)
|
||||||
|
|
||||||
data, _ := json.Marshal(result)
|
data, _ := json.Marshal(result)
|
||||||
var msgs []map[string]any
|
var msgs []map[string]any
|
||||||
|
|
@ -704,7 +704,7 @@ func TestSerializeMessages_MediaWithToolCallID(t *testing.T) {
|
||||||
messages := []protocoltypes.Message{
|
messages := []protocoltypes.Message{
|
||||||
{Role: "tool", Content: "image result", Media: []string{"data:image/png;base64,xyz"}, ToolCallID: "call_1"},
|
{Role: "tool", Content: "image result", Media: []string{"data:image/png;base64,xyz"}, ToolCallID: "call_1"},
|
||||||
}
|
}
|
||||||
result := common.SerializeMessages(messages)
|
result := serializeMessages(messages, false)
|
||||||
|
|
||||||
data, _ := json.Marshal(result)
|
data, _ := json.Marshal(result)
|
||||||
var msgs []map[string]any
|
var msgs []map[string]any
|
||||||
|
|
@ -834,7 +834,7 @@ func TestSerializeMessages_OmitsContentWhenEmptyAndToolCallsPresent(t *testing.T
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
result := serializeMessages(messages)
|
result := serializeMessages(messages, false)
|
||||||
|
|
||||||
data, _ := json.Marshal(result)
|
data, _ := json.Marshal(result)
|
||||||
var msgs []map[string]any
|
var msgs []map[string]any
|
||||||
|
|
@ -858,7 +858,7 @@ func TestSerializeMessages_IncludesContentWhenNonEmptyWithToolCalls(t *testing.T
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
result := serializeMessages(messages)
|
result := serializeMessages(messages, false)
|
||||||
|
|
||||||
data, _ := json.Marshal(result)
|
data, _ := json.Marshal(result)
|
||||||
var msgs []map[string]any
|
var msgs []map[string]any
|
||||||
|
|
@ -879,7 +879,7 @@ func TestSerializeMessages_StripsSystemParts(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
result := common.SerializeMessages(messages)
|
result := serializeMessages(messages, false)
|
||||||
|
|
||||||
data, _ := json.Marshal(result)
|
data, _ := json.Marshal(result)
|
||||||
raw := string(data)
|
raw := string(data)
|
||||||
|
|
@ -887,3 +887,125 @@ func TestSerializeMessages_StripsSystemParts(t *testing.T) {
|
||||||
t.Fatal("system_parts should not appear in serialized output")
|
t.Fatal("system_parts should not appear in serialized output")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSerializeMessages_StrictCompat_StripsReasoningContent(t *testing.T) {
|
||||||
|
messages := []protocoltypes.Message{
|
||||||
|
{Role: "user", Content: "What is 1+1?"},
|
||||||
|
{Role: "assistant", Content: "2", ReasoningContent: "Let me think... 1+1=2"},
|
||||||
|
}
|
||||||
|
result := serializeMessages(messages, true)
|
||||||
|
|
||||||
|
data, _ := json.Marshal(result)
|
||||||
|
var msgs []map[string]any
|
||||||
|
json.Unmarshal(data, &msgs)
|
||||||
|
|
||||||
|
if _, ok := msgs[1]["reasoning_content"]; ok {
|
||||||
|
t.Fatalf("reasoning_content should be stripped when strictCompat=true, got %v", msgs[1]["reasoning_content"])
|
||||||
|
}
|
||||||
|
if msgs[1]["content"] != "2" {
|
||||||
|
t.Fatalf("content should be preserved, got %v", msgs[1]["content"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSerializeMessages_StrictCompat_StripsExtraContent(t *testing.T) {
|
||||||
|
messages := []protocoltypes.Message{
|
||||||
|
{
|
||||||
|
Role: "assistant",
|
||||||
|
Content: "",
|
||||||
|
ToolCalls: []protocoltypes.ToolCall{
|
||||||
|
{
|
||||||
|
ID: "call_1",
|
||||||
|
Type: "function",
|
||||||
|
Function: &protocoltypes.FunctionCall{
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: `{"city":"SF"}`,
|
||||||
|
},
|
||||||
|
ExtraContent: &protocoltypes.ExtraContent{
|
||||||
|
Google: &protocoltypes.GoogleExtra{
|
||||||
|
ThoughtSignature: "sig123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result := serializeMessages(messages, true)
|
||||||
|
|
||||||
|
data, _ := json.Marshal(result)
|
||||||
|
raw := string(data)
|
||||||
|
if strings.Contains(raw, "extra_content") {
|
||||||
|
t.Fatalf("extra_content should be stripped when strictCompat=true, got: %s", raw)
|
||||||
|
}
|
||||||
|
if strings.Contains(raw, "sig123") {
|
||||||
|
t.Fatalf("thought_signature value should be stripped when strictCompat=true, got: %s", raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSerializeMessages_StrictCompat_StripsThoughtSignature(t *testing.T) {
|
||||||
|
messages := []protocoltypes.Message{
|
||||||
|
{
|
||||||
|
Role: "assistant",
|
||||||
|
Content: "",
|
||||||
|
ToolCalls: []protocoltypes.ToolCall{
|
||||||
|
{
|
||||||
|
ID: "call_1",
|
||||||
|
Type: "function",
|
||||||
|
Function: &protocoltypes.FunctionCall{
|
||||||
|
Name: "search",
|
||||||
|
Arguments: `{"query":"test"}`,
|
||||||
|
ThoughtSignature: "thought-sig-abc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result := serializeMessages(messages, true)
|
||||||
|
|
||||||
|
data, _ := json.Marshal(result)
|
||||||
|
raw := string(data)
|
||||||
|
if strings.Contains(raw, "thought_signature") {
|
||||||
|
t.Fatalf("thought_signature should be stripped when strictCompat=true, got: %s", raw)
|
||||||
|
}
|
||||||
|
if strings.Contains(raw, "thought-sig-abc") {
|
||||||
|
t.Fatalf("thought_signature value should be stripped when strictCompat=true, got: %s", raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSerializeMessages_NoStrictCompat_PreservesFields(t *testing.T) {
|
||||||
|
messages := []protocoltypes.Message{
|
||||||
|
{
|
||||||
|
Role: "assistant",
|
||||||
|
Content: "result",
|
||||||
|
ReasoningContent: "my reasoning",
|
||||||
|
ToolCalls: []protocoltypes.ToolCall{
|
||||||
|
{
|
||||||
|
ID: "call_1",
|
||||||
|
Type: "function",
|
||||||
|
Function: &protocoltypes.FunctionCall{
|
||||||
|
Name: "get_weather",
|
||||||
|
Arguments: `{"city":"SF"}`,
|
||||||
|
ThoughtSignature: "thought-sig-xyz",
|
||||||
|
},
|
||||||
|
ExtraContent: &protocoltypes.ExtraContent{
|
||||||
|
Google: &protocoltypes.GoogleExtra{
|
||||||
|
ThoughtSignature: "sig456",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result := serializeMessages(messages, false)
|
||||||
|
|
||||||
|
data, _ := json.Marshal(result)
|
||||||
|
raw := string(data)
|
||||||
|
if !strings.Contains(raw, "my reasoning") {
|
||||||
|
t.Fatalf("reasoning_content should be preserved when strictCompat=false, got: %s", raw)
|
||||||
|
}
|
||||||
|
if !strings.Contains(raw, "extra_content") {
|
||||||
|
t.Fatalf("extra_content should be preserved when strictCompat=false, got: %s", raw)
|
||||||
|
}
|
||||||
|
if !strings.Contains(raw, "sig456") {
|
||||||
|
t.Fatalf("thought_signature value in extra_content should be preserved when strictCompat=false, got: %s", raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue