fix(api): 缺失输出能力配置时透传请求
This commit is contained in:
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
const (
|
||||
volcesOutputTokenThreshold = 10240
|
||||
volcesOutputCapabilityErrorCode = "model_capability_configuration_error"
|
||||
volcesOutputInvalidParameterCode = "invalid_parameter"
|
||||
)
|
||||
|
||||
@@ -25,20 +24,25 @@ func (outputTokenLimitProcessor) ShouldProcess(_ map[string]any, modelType strin
|
||||
func (outputTokenLimitProcessor) Process(params map[string]any, modelType string, context *paramProcessContext) bool {
|
||||
modelMax, sourceType, capabilityValue, ok := candidateMaxOutputTokens(context.candidate, modelType)
|
||||
path := capabilityPath(sourceType, "max_output_tokens")
|
||||
if parameter, value, explicit := explicitOutputTokenParameter(context.kind, params); explicit {
|
||||
if _, valid := positiveInteger(value); !valid {
|
||||
return context.reject(
|
||||
"OutputTokenLimitProcessor",
|
||||
parameter,
|
||||
value,
|
||||
fmt.Sprintf("%s must be a positive integer", parameter),
|
||||
path,
|
||||
capabilityValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return context.reject(
|
||||
"OutputTokenLimitProcessor",
|
||||
outputTokenParameter(context.kind),
|
||||
nil,
|
||||
"火山引擎文本候选未配置正整数 max_output_tokens,已禁止执行该候选。",
|
||||
path,
|
||||
capabilityValue,
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
if context.kind == "chat.completions" {
|
||||
if value, explicit := nonNullParameter(params, "max_tokens"); explicit {
|
||||
if parsed, valid := positiveInteger(value); !valid || parsed > modelMax {
|
||||
if parsed, _ := positiveInteger(value); parsed > modelMax {
|
||||
return context.reject("OutputTokenLimitProcessor", "max_tokens", value, fmt.Sprintf("max_tokens must be a positive integer no greater than the selected Volcengine model limit (%d)", modelMax), path, capabilityValue)
|
||||
}
|
||||
return true
|
||||
@@ -57,7 +61,7 @@ func (outputTokenLimitProcessor) Process(params map[string]any, modelType string
|
||||
}
|
||||
|
||||
if value, explicit := nonNullParameter(params, "max_output_tokens"); explicit {
|
||||
if parsed, valid := positiveInteger(value); !valid || parsed > modelMax {
|
||||
if parsed, _ := positiveInteger(value); parsed > modelMax {
|
||||
return context.reject("OutputTokenLimitProcessor", "max_output_tokens", value, fmt.Sprintf("max_output_tokens must be a positive integer no greater than the selected Volcengine model limit (%d)", modelMax), path, capabilityValue)
|
||||
}
|
||||
return true
|
||||
@@ -76,8 +80,35 @@ func filterRuntimeCandidatesByOutputTokens(kind string, requestedModel string, m
|
||||
if !isOpenAITextGenerationKind(kind) || !isTextOutputModelType(modelType) || len(candidates) == 0 {
|
||||
return candidates, nil, nil
|
||||
}
|
||||
hasVolcesCandidate := false
|
||||
for _, candidate := range candidates {
|
||||
if isVolcesCandidate(candidate) {
|
||||
hasVolcesCandidate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasVolcesCandidate {
|
||||
return candidates, nil, nil
|
||||
}
|
||||
explicitParameter, explicitValue, hasExplicitParameter := explicitOutputTokenParameter(kind, body)
|
||||
explicitTokens := 0
|
||||
if hasExplicitParameter {
|
||||
var valid bool
|
||||
explicitTokens, valid = positiveInteger(explicitValue)
|
||||
if !valid {
|
||||
summary := map[string]any{
|
||||
"filter": "volces_output_token_limit", "kind": kind, "requestedModel": requestedModel, "modelType": modelType,
|
||||
"candidateCount": len(candidates), "supportedCandidateCount": 0, "filteredCandidateCount": len(candidates),
|
||||
"code": volcesOutputInvalidParameterCode, "reason": "invalid_output_token_parameter",
|
||||
"parameter": explicitParameter, "requestedValue": explicitValue,
|
||||
}
|
||||
message := fmt.Sprintf("%s must be a positive integer", explicitParameter)
|
||||
return nil, summary, &store.ModelCandidateUnavailableError{Code: volcesOutputInvalidParameterCode, Message: message, Details: summary}
|
||||
}
|
||||
}
|
||||
filtered := make([]store.RuntimeModelCandidate, 0, len(candidates))
|
||||
rejected := make([]map[string]any, 0)
|
||||
missingCapability := make([]map[string]any, 0)
|
||||
invalidExplicit := false
|
||||
for _, candidate := range candidates {
|
||||
if !isVolcesCandidate(candidate) {
|
||||
@@ -92,15 +123,15 @@ func filterRuntimeCandidatesByOutputTokens(kind string, requestedModel string, m
|
||||
}
|
||||
if !configured {
|
||||
detail["reason"] = "max_output_tokens_missing"
|
||||
rejected = append(rejected, detail)
|
||||
missingCapability = append(missingCapability, detail)
|
||||
filtered = append(filtered, candidate)
|
||||
continue
|
||||
}
|
||||
if parameter, value, explicit := explicitOutputTokenParameter(kind, body); explicit {
|
||||
parsed, valid := positiveInteger(value)
|
||||
if !valid || (parameter != "max_completion_tokens" && parsed > modelMax) {
|
||||
if hasExplicitParameter {
|
||||
if explicitParameter != "max_completion_tokens" && explicitTokens > modelMax {
|
||||
detail["reason"] = "requested_output_tokens_exceed_capability"
|
||||
detail["parameter"] = parameter
|
||||
detail["requestedValue"] = value
|
||||
detail["parameter"] = explicitParameter
|
||||
detail["requestedValue"] = explicitValue
|
||||
invalidExplicit = true
|
||||
rejected = append(rejected, detail)
|
||||
continue
|
||||
@@ -108,26 +139,25 @@ func filterRuntimeCandidatesByOutputTokens(kind string, requestedModel string, m
|
||||
}
|
||||
filtered = append(filtered, candidate)
|
||||
}
|
||||
if len(rejected) == 0 {
|
||||
if len(rejected) == 0 && len(missingCapability) == 0 {
|
||||
return filtered, nil, nil
|
||||
}
|
||||
summary := map[string]any{
|
||||
"filter": "volces_output_token_limit", "kind": kind, "requestedModel": requestedModel, "modelType": modelType,
|
||||
"candidateCount": len(candidates), "supportedCandidateCount": len(filtered), "filteredCandidateCount": len(rejected),
|
||||
"missingCapabilityCandidateCount": len(missingCapability), "passthroughMissingCapability": len(missingCapability) > 0,
|
||||
"threshold": volcesOutputTokenThreshold, "formula": "third=floor(modelMaxOutputTokens/3); default=third>=10240?third:modelMaxOutputTokens",
|
||||
"rejectedCandidates": rejected,
|
||||
"rejectedCandidates": rejected, "missingCapabilityCandidates": missingCapability,
|
||||
}
|
||||
if len(filtered) > 0 {
|
||||
return filtered, summary, nil
|
||||
}
|
||||
code := volcesOutputCapabilityErrorCode
|
||||
message := "所有火山引擎文本候选都缺少有效的 max_output_tokens 能力配置"
|
||||
if invalidExplicit {
|
||||
code = volcesOutputInvalidParameterCode
|
||||
message = "请求的输出 token 上限超过所有可用火山引擎候选的模型能力"
|
||||
message := "请求的输出 token 上限超过所有可用火山引擎候选的模型能力"
|
||||
summary["code"] = volcesOutputInvalidParameterCode
|
||||
return nil, summary, &store.ModelCandidateUnavailableError{Code: volcesOutputInvalidParameterCode, Message: message, Details: summary}
|
||||
}
|
||||
summary["code"] = code
|
||||
return nil, summary, &store.ModelCandidateUnavailableError{Code: code, Message: message, Details: summary}
|
||||
return filtered, summary, nil
|
||||
}
|
||||
|
||||
func defaultVolcesOutputTokens(modelMax int) int {
|
||||
@@ -195,13 +225,6 @@ func explicitOutputTokenParameter(kind string, body map[string]any) (string, any
|
||||
return "max_completion_tokens", value, ok
|
||||
}
|
||||
|
||||
func outputTokenParameter(kind string) string {
|
||||
if kind == "responses" {
|
||||
return "max_output_tokens"
|
||||
}
|
||||
return "max_tokens"
|
||||
}
|
||||
|
||||
func isOpenAITextGenerationKind(kind string) bool {
|
||||
return kind == "chat.completions" || kind == "responses"
|
||||
}
|
||||
|
||||
@@ -72,32 +72,90 @@ func TestVolcesOutputProcessorPreservesExplicitLimits(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolcesOutputCandidateFilterSupportsFailover(t *testing.T) {
|
||||
func TestVolcesOutputCandidateFilterPreservesMissingCapabilityOrder(t *testing.T) {
|
||||
missing := volcTextCandidate(nil)
|
||||
missing.PlatformID = "missing"
|
||||
valid := volcTextCandidate(32768)
|
||||
valid.PlatformID = "valid"
|
||||
filtered, summary, err := filterRuntimeCandidatesByOutputTokens("chat.completions", "demo", "text_generate", map[string]any{"messages": []any{}}, []store.RuntimeModelCandidate{missing, valid})
|
||||
if err != nil || len(filtered) != 1 || filtered[0].PlatformID != "valid" {
|
||||
t.Fatalf("expected missing capability candidate to be skipped: filtered=%+v summary=%+v err=%v", filtered, summary, err)
|
||||
if err != nil || len(filtered) != 2 || filtered[0].PlatformID != "missing" || filtered[1].PlatformID != "valid" {
|
||||
t.Fatalf("expected missing capability candidate order to be preserved: filtered=%+v summary=%+v err=%v", filtered, summary, err)
|
||||
}
|
||||
result := preprocessRequestWithLog("chat.completions", map[string]any{"messages": []any{}}, filtered[0])
|
||||
if summary["missingCapabilityCandidateCount"] != 1 || summary["passthroughMissingCapability"] != true {
|
||||
t.Fatalf("expected missing capability audit summary, got %+v", summary)
|
||||
}
|
||||
missingResult := preprocessRequestWithLog("chat.completions", map[string]any{"messages": []any{}}, filtered[0])
|
||||
if missingResult.Err != nil || len(missingResult.Log.Changes) != 0 {
|
||||
t.Fatalf("missing capability candidate must pass through unchanged: %+v", missingResult)
|
||||
}
|
||||
if _, ok := missingResult.Body["max_tokens"]; ok {
|
||||
t.Fatalf("missing capability candidate must not inject max_tokens: %+v", missingResult.Body)
|
||||
}
|
||||
result := preprocessRequestWithLog("chat.completions", map[string]any{"messages": []any{}}, filtered[1])
|
||||
if result.Body["max_tokens"] != 10922 {
|
||||
t.Fatalf("failover candidate must recalculate its own default, got %+v", result.Body)
|
||||
t.Fatalf("configured candidate must calculate its own default, got %+v", result.Body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolcesOutputCandidateFilterRejectsMissingAndExceededCapabilities(t *testing.T) {
|
||||
_, _, err := filterRuntimeCandidatesByOutputTokens("responses", "demo", "text_generate", map[string]any{"input": "hello"}, []store.RuntimeModelCandidate{volcTextCandidate(nil)})
|
||||
if store.ModelCandidateErrorCode(err) != volcesOutputCapabilityErrorCode {
|
||||
t.Fatalf("expected capability configuration error, got %v", err)
|
||||
func TestVolcesOutputCandidateFilterPassesThroughMissingCapability(t *testing.T) {
|
||||
filtered, summary, err := filterRuntimeCandidatesByOutputTokens("responses", "demo", "text_generate", map[string]any{"input": "hello"}, []store.RuntimeModelCandidate{volcTextCandidate(nil)})
|
||||
if err != nil || len(filtered) != 1 {
|
||||
t.Fatalf("expected missing capability candidate to remain available: filtered=%+v summary=%+v err=%v", filtered, summary, err)
|
||||
}
|
||||
_, _, err = filterRuntimeCandidatesByOutputTokens("chat.completions", "demo", "text_generate", map[string]any{"messages": []any{}, "max_tokens": 32769}, []store.RuntimeModelCandidate{volcTextCandidate(32768)})
|
||||
result := preprocessRequestWithLog("responses", map[string]any{"input": "hello"}, filtered[0])
|
||||
if result.Err != nil {
|
||||
t.Fatalf("missing capability preprocessing must pass through: %v", result.Err)
|
||||
}
|
||||
if _, ok := result.Body["max_output_tokens"]; ok {
|
||||
t.Fatalf("missing capability preprocessing must not inject max_output_tokens: %+v", result.Body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolcesOutputCandidateFilterRejectsExceededKnownCapability(t *testing.T) {
|
||||
_, _, err := filterRuntimeCandidatesByOutputTokens("chat.completions", "demo", "text_generate", map[string]any{"messages": []any{}, "max_tokens": 32769}, []store.RuntimeModelCandidate{volcTextCandidate(32768)})
|
||||
if store.ModelCandidateErrorCode(err) != volcesOutputInvalidParameterCode {
|
||||
t.Fatalf("expected invalid_parameter for explicit limit, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolcesOutputCandidateFilterAllowsUnknownCapabilityForExplicitLimit(t *testing.T) {
|
||||
missing := volcTextCandidate(nil)
|
||||
missing.PlatformID = "missing"
|
||||
configured := volcTextCandidate(32768)
|
||||
configured.PlatformID = "configured"
|
||||
filtered, summary, err := filterRuntimeCandidatesByOutputTokens(
|
||||
"chat.completions",
|
||||
"demo",
|
||||
"text_generate",
|
||||
map[string]any{"messages": []any{}, "max_tokens": 65536},
|
||||
[]store.RuntimeModelCandidate{missing, configured},
|
||||
)
|
||||
if err != nil || len(filtered) != 1 || filtered[0].PlatformID != "missing" {
|
||||
t.Fatalf("expected unknown capability candidate to remain available: filtered=%+v summary=%+v err=%v", filtered, summary, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVolcesOutputProcessorValidatesExplicitLimitWithoutCapability(t *testing.T) {
|
||||
valid := preprocessRequestWithLog("chat.completions", map[string]any{"messages": []any{}, "max_tokens": 2048}, volcTextCandidate(nil))
|
||||
if valid.Err != nil || valid.Body["max_tokens"] != 2048 {
|
||||
t.Fatalf("valid explicit limit must pass through: %+v", valid)
|
||||
}
|
||||
invalid := preprocessRequestWithLog("chat.completions", map[string]any{"messages": []any{}, "max_tokens": 0}, volcTextCandidate(nil))
|
||||
if invalid.Err == nil {
|
||||
t.Fatalf("invalid explicit limit must still be rejected: %+v", invalid)
|
||||
}
|
||||
_, _, err := filterRuntimeCandidatesByOutputTokens(
|
||||
"chat.completions",
|
||||
"demo",
|
||||
"text_generate",
|
||||
map[string]any{"messages": []any{}, "max_tokens": 0},
|
||||
[]store.RuntimeModelCandidate{volcTextCandidate(nil)},
|
||||
)
|
||||
if store.ModelCandidateErrorCode(err) != volcesOutputInvalidParameterCode {
|
||||
t.Fatalf("candidate filter must reject an invalid explicit limit, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonVolcesOutputProcessorDoesNotInject(t *testing.T) {
|
||||
candidate := volcTextCandidate(131072)
|
||||
candidate.Provider = "openai"
|
||||
|
||||
Reference in New Issue
Block a user