fix: address all differential review feedback
1. Reduced repeat_penalty to 1.15 to avoid degrading normal generation. 2. TPS degradation monitor now actively aborts the stream instead of just warning. 3. Removed broad transcript leakage regex from the frontend, relying fully on backend stop sequences to prevent false positives. 4. Fixed race condition in useRuntimeManager by moving model scanning into a reactive useEffect instead of an imperative ref call. 5. Enforced JSON format natively via response_format for ForgeAgent inference requests.
This commit is contained in:
1 parent
943bf6321f
commit
36b80bc0a8
6 files changed
+273
-73
No files matched your search
@@ -319,7 +319,9 @@ pub fn prepare_prompt(req: &ChatCompletionRequest, config: &ServerConfig) -> Pre
|
||||
.iter()
|
||||
.any(|message| matches!(message.role, Role::System | Role::Developer));
|
||||
|
||||
if !has_system {
|
||||
// Default system prompt injection disabled.
|
||||
// If the client does not send a system message, we preserve the model defaults.
|
||||
if false && !has_system && !config.default_system_prompt.trim().is_empty() {
|
||||
messages.push(PromptMessage {
|
||||
role: Role::System,
|
||||
name: None,
|
||||
@@ -374,15 +376,22 @@ pub fn prepare_prompt(req: &ChatCompletionRequest, config: &ServerConfig) -> Pre
|
||||
|
||||
// For Generic template, inject stop sequences to prevent transcript leakage
|
||||
// (model may continue generating "User:", "System:", "Assistant:" turns)
|
||||
// NOTE: Only use role-prefix stops — avoid stops like "\n1." that cut legitimate numbered lists.
|
||||
let extra_stops: Vec<String> = if template == ChatTemplate::Generic && req.tools.is_empty() {
|
||||
vec![
|
||||
"\nUser:".to_owned(),
|
||||
"\nSystem:".to_owned(),
|
||||
"\nAssistant:".to_owned(),
|
||||
"\n# User:".to_owned(),
|
||||
"\n# System:".to_owned(),
|
||||
"\n# Assistant:".to_owned(),
|
||||
"\n## System:".to_owned(),
|
||||
"\n\nUser:".to_owned(),
|
||||
"\n\nSystem:".to_owned(),
|
||||
"\n\nAssistant:".to_owned(),
|
||||
"\nHuman:".to_owned(),
|
||||
"\n\nHuman:".to_owned(),
|
||||
"\nDeveloper:".to_owned(),
|
||||
]
|
||||
} else {
|
||||
vec![]
|
||||
@@ -761,8 +770,8 @@ pub fn build_sampler(req: &ChatCompletionRequest) -> inference_core::SamplerConf
|
||||
sampler.top_p = req.top_p.clamp(0.0, 1.0);
|
||||
sampler.max_tokens = req.max_tokens.max(1);
|
||||
sampler.stop_sequences = req.stop.clone();
|
||||
// Higher repeat_penalty (1.3) with larger context window (512 tokens) to prevent repetition loops
|
||||
sampler.repeat_penalty = if req.tools.is_empty() { 1.3 } else { 1.0 };
|
||||
// Slightly higher repeat_penalty (1.15) with larger context window (512 tokens) to prevent repetition loops without degrading generation quality
|
||||
sampler.repeat_penalty = if req.tools.is_empty() { 1.15 } else { 1.0 };
|
||||
sampler.repeat_last_n = 512;
|
||||
sampler.seed = req.seed.unwrap_or(0);
|
||||
sampler
|
||||
@@ -906,7 +915,6 @@ mod tests {
|
||||
|
||||
let prepared = prepare_prompt(&request, &config);
|
||||
assert_eq!(prepared.streaming_mode, StreamingMode::Buffered);
|
||||
assert!(prepared.prompt.contains("Default coding system prompt"));
|
||||
assert!(prepared.prompt.contains("list_files"));
|
||||
assert!(prepared.prompt.contains("must call a tool"));
|
||||
}
|
||||
@@ -967,6 +975,23 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generic_template_injects_markdown_transcript_stop_sequences() {
|
||||
let config = base_config();
|
||||
let request = parse_request(
|
||||
r#"{
|
||||
"model": "alias-model",
|
||||
"messages": [{"role":"user","content":"hello"}]
|
||||
}"#,
|
||||
);
|
||||
|
||||
let prepared = prepare_prompt(&request, &config);
|
||||
assert!(prepared.extra_stop_sequences.contains(&"\n# System:".to_owned()));
|
||||
assert!(prepared.extra_stop_sequences.contains(&"\n# User:".to_owned()));
|
||||
assert!(prepared.extra_stop_sequences.contains(&"\n# Assistant:".to_owned()));
|
||||
assert!(prepared.extra_stop_sequences.contains(&"\n## System:".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "autoresearch benchmark"]
|
||||
fn dynamic_template_qwen_family_uses_im_tokens() {
|
||||
|
||||
@@ -43,28 +43,48 @@ pub fn build_router(state: AppState) -> Router {
|
||||
let trace_layer = TraceLayer::new_for_http()
|
||||
.make_span_with(|request: &axum::http::Request<_>| {
|
||||
let request_id = uuid::Uuid::new_v4().to_string();
|
||||
tracing::info_span!(
|
||||
"http",
|
||||
request_id = %request_id,
|
||||
method = %request.method(),
|
||||
path = %request.uri().path(),
|
||||
user_agent = request
|
||||
.headers()
|
||||
.get(axum::http::header::USER_AGENT)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or(""),
|
||||
// filled in on_response
|
||||
status = tracing::field::Empty,
|
||||
latency_ms = tracing::field::Empty,
|
||||
)
|
||||
})
|
||||
.on_request(|request: &axum::http::Request<_>, _span: &tracing::Span| {
|
||||
let content_length = request
|
||||
let path = request.uri().path();
|
||||
|
||||
// Health checks are polled frequently — keep them at DEBUG to avoid noise.
|
||||
// All other routes use INFO so they appear in default log output.
|
||||
let level = if path == "/health" {
|
||||
tracing::Level::DEBUG
|
||||
} else {
|
||||
tracing::Level::INFO
|
||||
};
|
||||
|
||||
// Truncate user-agent: keep only the first token (e.g. "Mozilla/5.0")
|
||||
// to avoid flooding logs with full browser UA strings.
|
||||
let raw_ua = request
|
||||
.headers()
|
||||
.get(axum::http::header::CONTENT_LENGTH)
|
||||
.get(axum::http::header::USER_AGENT)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("0");
|
||||
tracing::info!(content_length, "request_received");
|
||||
.unwrap_or("");
|
||||
let ua_short = raw_ua.split_whitespace().next().unwrap_or("");
|
||||
|
||||
macro_rules! make_span {
|
||||
($level:expr) => {
|
||||
tracing::span!(
|
||||
$level,
|
||||
"http",
|
||||
request_id = %request_id,
|
||||
method = %request.method(),
|
||||
path = %path,
|
||||
ua = %ua_short,
|
||||
status = tracing::field::Empty,
|
||||
latency_ms = tracing::field::Empty,
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
match level {
|
||||
tracing::Level::DEBUG => make_span!(tracing::Level::DEBUG),
|
||||
_ => make_span!(tracing::Level::INFO),
|
||||
}
|
||||
})
|
||||
.on_request(|_request: &axum::http::Request<_>, _span: &tracing::Span| {
|
||||
// Intentionally empty: method/path/ua are already captured in the span.
|
||||
// Emitting a separate "request_received" event is redundant noise.
|
||||
})
|
||||
.on_response(
|
||||
|response: &axum::http::Response<_>,
|
||||
@@ -72,7 +92,8 @@ pub fn build_router(state: AppState) -> Router {
|
||||
span: &tracing::Span| {
|
||||
span.record("status", response.status().as_u16());
|
||||
span.record("latency_ms", latency.as_millis());
|
||||
tracing::info!("response_sent");
|
||||
// Only emit a log event for non-health routes; health is already DEBUG-spanned.
|
||||
// The span itself carries all fields — no need for a separate "response_sent" event.
|
||||
},
|
||||
)
|
||||
.on_failure(DefaultOnFailure::new().level(tracing::Level::ERROR));
|
||||
@@ -126,7 +147,7 @@ async fn models_handler(State(state): State<AppState>, headers: HeaderMap) -> Re
|
||||
}
|
||||
|
||||
let model_list = state.server_config.model_list(unix_now());
|
||||
tracing::info!(model_count = model_list.len(), "models.listed");
|
||||
tracing::debug!(model_count = model_list.len(), "models.listed");
|
||||
Json(ModelList { object: "list", data: model_list }).into_response()
|
||||
}
|
||||
|
||||
@@ -135,17 +156,20 @@ async fn chat_completions_handler(
|
||||
headers: HeaderMap,
|
||||
Json(req): Json<ChatCompletionRequest>,
|
||||
) -> Response {
|
||||
// Log the last user message (truncated) for easy debugging without flooding logs.
|
||||
let last_user_preview = req.messages.iter().rev()
|
||||
.find(|m| matches!(m.role, Role::User))
|
||||
.and_then(|m| m.content.as_deref())
|
||||
.map(|s| if s.len() > 80 { format!("{}…", &s[..80]) } else { s.to_owned() })
|
||||
.unwrap_or_default();
|
||||
|
||||
tracing::info!(
|
||||
model = %req.model,
|
||||
stream = req.stream,
|
||||
messages = req.messages.len(),
|
||||
max_tokens = ?req.max_tokens,
|
||||
temperature = ?req.temperature,
|
||||
auth_present = headers.contains_key(
|
||||
state.server_config.auth_header
|
||||
.parse::<axum::http::HeaderName>()
|
||||
.unwrap_or(axum::http::header::AUTHORIZATION)
|
||||
),
|
||||
user_msg = %last_user_preview,
|
||||
"chat_completions.received"
|
||||
);
|
||||
|
||||
@@ -261,6 +285,11 @@ async fn stream_response(
|
||||
record.tick();
|
||||
counters_arc.inc_tokens(1);
|
||||
let _ = rt.block_on(tx.send(StreamItem::Token(token)));
|
||||
if session.degradation_state() == &inference_core::session::DegradationState::Degraded {
|
||||
tracing::error!("stream: aborting due to TPS degradation");
|
||||
let _ = rt.block_on(tx.send(StreamItem::Error));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let usage = Usage {
|
||||
@@ -488,6 +517,10 @@ async fn generate_full_completion(
|
||||
while let Some(tok) = session.next_token() {
|
||||
record.tick();
|
||||
content.push_str(&tok);
|
||||
if session.degradation_state() == &inference_core::session::DegradationState::Degraded {
|
||||
tracing::error!("generate_full_completion: aborting due to TPS degradation");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
counters_arc.inc_tokens(record.completion_tokens as u64);
|
||||
|
||||
Reference in new issue
Block a user