fix: UTF-8 safe truncation, deduplicate url_encode, add Display impls, fix pricing display, simplify model matching, fix changelog links, add PartialEq for Tool (#3)
This commit is contained in:
+2
-1
@@ -324,7 +324,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Hooks system and config profiles
|
||||
- Example skills and launch assets
|
||||
|
||||
[Unreleased]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.3.22...HEAD
|
||||
[Unreleased]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.3.23...HEAD
|
||||
[0.3.23]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.3.22...v0.3.23
|
||||
[0.3.22]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.3.21...v0.3.22
|
||||
[0.3.21]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.3.17...v0.3.21
|
||||
[0.3.17]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.3.16...v0.3.17
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
//! Not yet wired into consumers; will be adopted incrementally.
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use crate::llm_client::LlmError;
|
||||
use crate::tools::spec::ToolError;
|
||||
|
||||
@@ -42,6 +44,44 @@ pub struct ErrorEnvelope {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for ErrorCategory {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let label = match self {
|
||||
Self::Network => "network",
|
||||
Self::Authentication => "authentication",
|
||||
Self::Authorization => "authorization",
|
||||
Self::RateLimit => "rate_limit",
|
||||
Self::Timeout => "timeout",
|
||||
Self::InvalidInput => "invalid_input",
|
||||
Self::Parse => "parse",
|
||||
Self::Tool => "tool",
|
||||
Self::State => "state",
|
||||
Self::Internal => "internal",
|
||||
};
|
||||
f.write_str(label)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ErrorSeverity {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let label = match self {
|
||||
Self::Info => "info",
|
||||
Self::Warning => "warning",
|
||||
Self::Error => "error",
|
||||
Self::Critical => "critical",
|
||||
};
|
||||
f.write_str(label)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ErrorEnvelope {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "[{}] {}: {}", self.severity, self.code, self.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ErrorEnvelope {}
|
||||
|
||||
impl ErrorEnvelope {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
|
||||
+16
-1
@@ -2,8 +2,10 @@
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Lifecycle stage for a feature flag.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -32,6 +34,19 @@ pub enum Feature {
|
||||
ExecPolicy,
|
||||
}
|
||||
|
||||
impl fmt::Display for Stage {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let label = match self {
|
||||
Self::Experimental => "experimental",
|
||||
Self::Beta => "beta",
|
||||
Self::Stable => "stable",
|
||||
Self::Deprecated => "deprecated",
|
||||
Self::Removed => "removed",
|
||||
};
|
||||
f.write_str(label)
|
||||
}
|
||||
}
|
||||
|
||||
impl Feature {
|
||||
pub fn key(self) -> &'static str {
|
||||
self.info().key
|
||||
|
||||
+2
-10
@@ -124,7 +124,7 @@ pub struct ToolCaller {
|
||||
}
|
||||
|
||||
/// Tool definition exposed to the model.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct Tool {
|
||||
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
|
||||
pub tool_type: Option<String>,
|
||||
@@ -188,15 +188,7 @@ pub struct Usage {
|
||||
#[must_use]
|
||||
pub fn context_window_for_model(model: &str) -> Option<u32> {
|
||||
let lower = model.to_lowercase();
|
||||
if lower.contains("deepseek-v3.2") {
|
||||
return Some(DEFAULT_CONTEXT_WINDOW_TOKENS);
|
||||
}
|
||||
if lower.contains("deepseek-chat")
|
||||
|| lower.contains("deepseek-reasoner")
|
||||
|| lower.contains("deepseek-r1")
|
||||
{
|
||||
return Some(DEFAULT_CONTEXT_WINDOW_TOKENS);
|
||||
}
|
||||
// All DeepSeek models currently share the same 128k context window.
|
||||
if lower.contains("deepseek") {
|
||||
return Some(DEFAULT_CONTEXT_WINDOW_TOKENS);
|
||||
}
|
||||
|
||||
+1
-1
@@ -101,7 +101,7 @@ pub fn calculate_turn_cost(model: &str, input_tokens: u32, output_tokens: u32) -
|
||||
#[must_use]
|
||||
pub fn format_cost(cost: f64) -> String {
|
||||
if cost < 0.0001 {
|
||||
"<$0.01".to_string()
|
||||
"<$0.0001".to_string()
|
||||
} else if cost < 0.01 {
|
||||
format!("${:.4}", cost)
|
||||
} else if cost < 1.0 {
|
||||
|
||||
+1
-14
@@ -3,6 +3,7 @@
|
||||
use super::spec::{
|
||||
ApprovalRequirement, ToolCapability, ToolContext, ToolError, ToolResult, ToolSpec, required_str,
|
||||
};
|
||||
use crate::utils::url_encode;
|
||||
use async_trait::async_trait;
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
@@ -334,17 +335,3 @@ fn normalize_stooq_symbol(ticker: &str, market: &str) -> String {
|
||||
fn parse_f64(input: &str) -> Option<f64> {
|
||||
input.parse::<f64>().ok()
|
||||
}
|
||||
|
||||
fn url_encode(input: &str) -> String {
|
||||
let mut encoded = String::new();
|
||||
for ch in input.bytes() {
|
||||
match ch {
|
||||
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
|
||||
encoded.push(ch as char)
|
||||
}
|
||||
b' ' => encoded.push('+'),
|
||||
_ => encoded.push_str(&format!("%{ch:02X}")),
|
||||
}
|
||||
}
|
||||
encoded
|
||||
}
|
||||
|
||||
+1
-14
@@ -4,6 +4,7 @@ use super::spec::{
|
||||
ApprovalRequirement, ToolCapability, ToolContext, ToolError, ToolResult, ToolSpec,
|
||||
optional_str, optional_u64, required_str,
|
||||
};
|
||||
use crate::utils::url_encode;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{NaiveDate, Utc};
|
||||
use serde::Serialize;
|
||||
@@ -320,17 +321,3 @@ async fn fetch_forecast(
|
||||
fn c_to_f(c: f64) -> f64 {
|
||||
c * 9.0 / 5.0 + 32.0
|
||||
}
|
||||
|
||||
fn url_encode(input: &str) -> String {
|
||||
let mut encoded = String::new();
|
||||
for ch in input.bytes() {
|
||||
match ch {
|
||||
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
|
||||
encoded.push(ch as char)
|
||||
}
|
||||
b' ' => encoded.push('+'),
|
||||
_ => encoded.push_str(&format!("%{ch:02X}")),
|
||||
}
|
||||
}
|
||||
encoded
|
||||
}
|
||||
|
||||
+32
-5
@@ -187,15 +187,42 @@ pub fn output_path(output_dir: &Path, filename: &str) -> PathBuf {
|
||||
output_dir.join(filename)
|
||||
}
|
||||
|
||||
/// Truncate a string to a maximum length, adding an ellipsis if truncated
|
||||
/// Truncate a string to a maximum length, adding an ellipsis if truncated.
|
||||
///
|
||||
/// Uses char boundaries to avoid panicking on multi-byte UTF-8 characters.
|
||||
#[must_use]
|
||||
pub fn truncate_with_ellipsis(s: &str, max_len: usize, ellipsis: &str) -> String {
|
||||
if s.len() <= max_len {
|
||||
s.to_string()
|
||||
} else {
|
||||
let truncate_at = max_len.saturating_sub(ellipsis.len());
|
||||
format!("{}{}", &s[..truncate_at], ellipsis)
|
||||
return s.to_string();
|
||||
}
|
||||
let budget = max_len.saturating_sub(ellipsis.len());
|
||||
// Find the last char boundary that fits within the byte budget.
|
||||
let safe_end = s
|
||||
.char_indices()
|
||||
.map(|(i, _)| i)
|
||||
.take_while(|&i| i <= budget)
|
||||
.last()
|
||||
.unwrap_or(0);
|
||||
format!("{}{}", &s[..safe_end], ellipsis)
|
||||
}
|
||||
|
||||
/// Percent-encode a string for use in URL query parameters.
|
||||
///
|
||||
/// Encodes all characters except unreserved characters (A-Z, a-z, 0-9, `-`, `_`, `.`, `~`).
|
||||
/// Spaces are encoded as `+`.
|
||||
#[must_use]
|
||||
pub fn url_encode(input: &str) -> String {
|
||||
let mut encoded = String::new();
|
||||
for ch in input.bytes() {
|
||||
match ch {
|
||||
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
|
||||
encoded.push(ch as char)
|
||||
}
|
||||
b' ' => encoded.push('+'),
|
||||
_ => encoded.push_str(&format!("%{ch:02X}")),
|
||||
}
|
||||
}
|
||||
encoded
|
||||
}
|
||||
|
||||
/// Estimate the total character count across message content blocks.
|
||||
|
||||
Reference in New Issue
Block a user