use std::{collections::HashMap, fmt::Write};
use anyhow::Result;
use serde_json::Value;
use crate::chat_template::{ChatTemplateParams, ChatTemplateState};
const TS_INDENT: &str = " ";
const TS_FIELD_DELIMITER: &str = ",\n";
const MAX_RECURSION_DEPTH: usize = 32;
pub fn encode_tools_to_typescript(tools: &[Value]) -> Option<String> {
if tools.is_empty() {
return None;
}
let mut function_strs: Vec<String> = Vec::new();
for t in tools {
if t.get("type").and_then(Value::as_str) != Some("function") {
continue;
}
let func = match t.get("function") {
Some(f) if f.as_object().is_some_and(|o| !o.is_empty()) => f,
_ => continue,
};
match openai_function_to_typescript(func) {
Some(s) => function_strs.push(s),
None => {
tracing::warn!(
"Kimi-K2.5 tool schema unsupported by the TypeScript encoder; \
leaving tools_ts_str undefined so the template falls back to JSON \
tool declarations"
);
return None;
}
}
}
if function_strs.is_empty() {
return None;
}
let mut out = String::from("# Tools\n\n## functions\nnamespace functions {\n");
out.push_str(&function_strs.join("\n"));
out.push('\n');
out.push_str("}\n");
Some(out)
}
pub(crate) fn apply_kimi_k25_tools(
chat_template: &ChatTemplateState,
messages: &[Value],
params: ChatTemplateParams,
) -> Result<String> {
let ts_str = params.tools.and_then(encode_tools_to_typescript);
let owned: Option<HashMap<String, Value>> = match (params.template_kwargs, ts_str.as_ref()) {
(Some(existing), Some(ts)) => {
let mut m = existing.clone();
m.insert("tools_ts_str".to_string(), Value::String(ts.clone()));
Some(m)
}
(None, Some(ts)) => {
let mut m = HashMap::with_capacity(1);
m.insert("tools_ts_str".to_string(), Value::String(ts.clone()));
Some(m)
}
_ => None, };
let new_params = ChatTemplateParams {
template_kwargs: owned.as_ref().or(params.template_kwargs),
..params
};
chat_template.apply(messages, new_params)
}
fn openai_function_to_typescript(function: &Value) -> Option<String> {
let parameters = function
.get("parameters")
.cloned()
.unwrap_or_else(|| Value::Object(Default::default()));
let mut registry = SchemaRegistry::default();
let parsed = ParameterTypeObject::parse(¶meters, &mut registry);
let mut interfaces: Vec<String> = Vec::new();
let mut root_interface_name: Option<&str> = None;
if registry.has_self_ref {
root_interface_name = Some("parameters");
let body = parsed
.properties
.iter()
.map(|p| p.to_typescript(TS_INDENT, ®istry))
.collect::<Vec<_>>()
.join(TS_FIELD_DELIMITER);
let body = if body.is_empty() {
String::new()
} else {
format!("\n{body}\n")
};
interfaces.push(format!("interface parameters {{{body}}}"));
}
let defs_ordered: Vec<(String, Value)> = registry
.order
.iter()
.filter_map(|name| {
registry
.definitions
.get(name)
.map(|v| (name.clone(), v.clone()))
})
.collect();
for (name, schema) in defs_ordered {
let obj_type = parse_parameter_type(&schema, &mut registry);
let body = obj_type.to_typescript("", ®istry);
let mut def_str = String::new();
if let Some(desc) = schema.get("description").and_then(Value::as_str) {
if !desc.is_empty() {
def_str.push_str(&format_description(desc, ""));
def_str.push('\n');
}
}
#[expect(
clippy::unwrap_used,
reason = "write!/writeln! into String cannot fail"
)]
{
write!(def_str, "interface {name} {body}").unwrap();
}
interfaces.push(def_str);
}
let interface_str = interfaces.join("\n");
let function_name = function
.get("name")
.and_then(Value::as_str)
.unwrap_or("function");
let type_def = match root_interface_name {
Some(n) => format!("type {function_name} = (_: {n}) => any;"),
None => format!(
"type {function_name} = (_: {}) => any;",
parsed.to_typescript("", ®istry)
),
};
let description = function
.get("description")
.and_then(Value::as_str)
.unwrap_or("");
let desc_block = if description.is_empty() {
String::new()
} else {
format_description(description, "")
};
if registry.unsupported {
return None;
}
Some(
[interface_str, desc_block, type_def]
.into_iter()
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("\n"),
)
}
#[derive(Default)]
struct SchemaRegistry {
definitions: HashMap<String, Value>,
order: Vec<String>,
has_self_ref: bool,
depth: usize,
unsupported: bool,
}
impl SchemaRegistry {
fn register_definitions(&mut self, defs: &Value) {
if let Some(map) = defs.as_object() {
for (name, schema) in map {
if !self.definitions.contains_key(name) {
self.order.push(name.clone());
}
self.definitions.insert(name.clone(), schema.clone());
}
}
}
fn resolve_ref(&mut self, reference: &str) -> Option<Value> {
if reference == "#" {
self.has_self_ref = true;
return Some(serde_json::json!({"$self_ref": true}));
}
if let Some(name) = reference.strip_prefix("#/$defs/") {
match self.definitions.get(name).cloned() {
Some(v) => return Some(v),
None => {
self.unsupported = true;
return None;
}
}
}
self.unsupported = true;
None
}
}
enum ParameterType {
Scalar(ParameterTypeScalar),
Object(ParameterTypeObject),
Array(ParameterTypeArray),
Enum(ParameterTypeEnum),
AnyOf(ParameterTypeAnyOf),
Union(ParameterTypeUnion),
Ref(ParameterTypeRef),
}
impl ParameterType {
fn format_docstring(&self, indent: &str) -> String {
match self {
ParameterType::Scalar(s) => s.base.format_docstring(indent),
ParameterType::Object(o) => o.base.format_docstring(indent),
ParameterType::Array(a) => a.base.format_docstring(indent),
ParameterType::Enum(e) => e.base.format_docstring(indent),
ParameterType::AnyOf(a) => a.base.format_docstring(indent),
ParameterType::Union(u) => u.base.format_docstring(indent),
ParameterType::Ref(r) => r.base.format_docstring(indent),
}
}
fn to_typescript(&self, indent: &str, registry: &SchemaRegistry) -> String {
match self {
ParameterType::Scalar(s) => s.to_typescript(),
ParameterType::Object(o) => o.to_typescript(indent, registry),
ParameterType::Array(a) => a.to_typescript(indent, registry),
ParameterType::Enum(e) => e.to_typescript(),
ParameterType::AnyOf(a) => a.to_typescript(indent, registry),
ParameterType::Union(u) => u.to_typescript(),
ParameterType::Ref(r) => r.to_typescript(),
}
}
}
#[derive(Default)]
struct BaseType {
description: String,
constraints: Vec<(String, Value)>,
}
impl BaseType {
fn from_extra_props(props: &Value, allowed_keys: &[&str]) -> Self {
let description = props
.get("description")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let mut constraints: Vec<(String, Value)> = props
.as_object()
.map(|m| {
m.iter()
.filter(|(k, _)| allowed_keys.contains(&k.as_str()))
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
})
.unwrap_or_default();
constraints.sort_by(|a, b| a.0.cmp(&b.0));
Self {
description,
constraints,
}
}
fn format_docstring(&self, indent: &str) -> String {
let mut out = String::new();
if !self.description.is_empty() {
out.push_str(&format_description(&self.description, indent));
out.push('\n');
}
if !self.constraints.is_empty() {
let parts: Vec<String> = self
.constraints
.iter()
.map(|(k, v)| format!("{k}: {}", json_inline(v)))
.collect();
#[expect(
clippy::unwrap_used,
reason = "write!/writeln! into String cannot fail"
)]
{
writeln!(out, "{indent}// {}", parts.join(", ")).unwrap();
}
}
out
}
}
struct ParameterTypeScalar {
base: BaseType,
typ: String,
}
impl ParameterTypeScalar {
fn parse(typ: &str, props: &Value) -> Self {
let allowed: &[&str] = match typ {
"string" => &["maxLength", "minLength", "pattern"],
"number" | "integer" => &["maximum", "minimum"],
_ => &[],
};
Self {
base: BaseType::from_extra_props(props, allowed),
typ: typ.to_string(),
}
}
fn any() -> Self {
Self {
base: BaseType::default(),
typ: "any".to_string(),
}
}
fn to_typescript(&self) -> String {
match self.typ.as_str() {
"integer" => "number".to_string(),
other => other.to_string(),
}
}
}
struct Parameter {
name: String,
typ: ParameterType,
optional: bool,
default: Option<Value>,
}
impl Parameter {
fn to_typescript(&self, indent: &str, registry: &SchemaRegistry) -> String {
let mut out = self.typ.format_docstring(indent);
if let Some(d) = &self.default {
let repr = match d {
Value::Bool(true) => "True".to_string(),
Value::Bool(false) => "False".to_string(),
Value::Number(_) => d.to_string(),
_ => serde_json::to_string(d).unwrap_or_else(|_| "null".to_string()),
};
#[expect(
clippy::unwrap_used,
reason = "write!/writeln! into String cannot fail"
)]
{
writeln!(out, "{indent}// Default: {repr}").unwrap();
}
}
let opt_marker = if self.optional { "?" } else { "" };
#[expect(
clippy::unwrap_used,
reason = "write!/writeln! into String cannot fail"
)]
{
write!(
out,
"{indent}{}{opt_marker}: {}",
self.name,
self.typ.to_typescript(indent, registry)
)
.unwrap();
}
out
}
}
struct ParameterTypeObject {
base: BaseType,
properties: Vec<Parameter>,
additional_properties: AdditionalProperties,
}
enum AdditionalProperties {
None,
True,
False,
Schema(Box<ParameterType>),
}
impl ParameterTypeObject {
fn parse(schema: &Value, registry: &mut SchemaRegistry) -> Self {
let base = BaseType::from_extra_props(schema, &[]);
if let Some(defs) = schema.get("$defs") {
registry.register_definitions(defs);
}
let additional_properties = match schema.get("additionalProperties") {
None => AdditionalProperties::None,
Some(Value::Bool(true)) => AdditionalProperties::True,
Some(Value::Bool(false)) => AdditionalProperties::False,
Some(other) => {
AdditionalProperties::Schema(Box::new(parse_parameter_type(other, registry)))
}
};
let props_map = schema.get("properties").and_then(Value::as_object);
let required: Vec<&str> = schema
.get("required")
.and_then(Value::as_array)
.map(|arr| arr.iter().filter_map(Value::as_str).collect())
.unwrap_or_default();
let properties: Vec<Parameter> = props_map
.map(|props| {
props
.iter()
.map(|(name, prop_schema)| {
let optional = !required.contains(&name.as_str());
let default = prop_schema.get("default").cloned();
let typ = parse_parameter_type(prop_schema, registry);
Parameter {
name: name.clone(),
typ,
optional,
default,
}
})
.collect()
})
.unwrap_or_default();
Self {
base,
properties,
additional_properties,
}
}
fn to_typescript(&self, indent: &str, registry: &SchemaRegistry) -> String {
let mut required: Vec<&Parameter> =
self.properties.iter().filter(|p| !p.optional).collect();
let mut optional: Vec<&Parameter> = self.properties.iter().filter(|p| p.optional).collect();
required.sort_by(|a, b| a.name.cmp(&b.name));
optional.sort_by(|a, b| a.name.cmp(&b.name));
let inner_indent = format!("{indent}{TS_INDENT}");
let mut parts: Vec<String> = required
.into_iter()
.chain(optional)
.map(|p| p.to_typescript(&inner_indent, registry))
.collect();
match &self.additional_properties {
AdditionalProperties::None => {}
AdditionalProperties::True => parts.push(format!("{inner_indent}[k: string]: any")),
AdditionalProperties::False => parts.push(format!("{inner_indent}[k: string]: never")),
AdditionalProperties::Schema(inner) => {
let ty = inner.to_typescript(&inner_indent, registry);
parts.push(format!("{inner_indent}[k: string]: {ty}"));
}
}
if parts.is_empty() {
return "{}".to_string();
}
let body = parts.join(TS_FIELD_DELIMITER);
let body = format!("\n{body}\n");
format!("{{{body}{indent}}}")
}
}
struct ParameterTypeArray {
base: BaseType,
item: Box<ParameterType>,
}
impl ParameterTypeArray {
fn parse(schema: &Value, registry: &mut SchemaRegistry) -> Self {
let base = BaseType::from_extra_props(schema, &["minItems", "maxItems"]);
let item = match schema.get("items") {
Some(items) if !items.is_null() => parse_parameter_type(items, registry),
_ => ParameterType::Scalar(ParameterTypeScalar::any()),
};
Self {
base,
item: Box::new(item),
}
}
fn to_typescript(&self, indent: &str, registry: &SchemaRegistry) -> String {
let inner_indent = format!("{indent}{TS_INDENT}");
let item_doc = self.item.format_docstring(&inner_indent);
let item_ts = self.item.to_typescript(&inner_indent, registry);
if item_doc.is_empty() {
format!("Array<{item_ts}>")
} else {
format!("Array<\n{item_doc}{inner_indent}{item_ts}\n{indent}>")
}
}
}
struct ParameterTypeEnum {
base: BaseType,
values: Vec<Value>,
}
impl ParameterTypeEnum {
fn parse(schema: &Value) -> Self {
let values = schema
.get("enum")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
Self {
base: BaseType::from_extra_props(schema, &[]),
values,
}
}
fn to_typescript(&self) -> String {
self.values
.iter()
.map(|v| match v {
Value::String(s) => format!("\"{s}\""),
Value::Null => "None".to_string(),
Value::Bool(true) => "True".to_string(),
Value::Bool(false) => "False".to_string(),
other => other.to_string(),
})
.collect::<Vec<_>>()
.join(" | ")
}
}
struct ParameterTypeAnyOf {
base: BaseType,
branches: Vec<ParameterType>,
}
impl ParameterTypeAnyOf {
fn parse(schema: &Value, registry: &mut SchemaRegistry) -> Self {
let branches = schema
.get("anyOf")
.and_then(Value::as_array)
.map(|arr| {
arr.iter()
.map(|s| parse_parameter_type(s, registry))
.collect()
})
.unwrap_or_default();
Self {
base: BaseType::from_extra_props(schema, &[]),
branches,
}
}
fn to_typescript(&self, indent: &str, registry: &SchemaRegistry) -> String {
self.branches
.iter()
.map(|b| b.to_typescript(indent, registry))
.collect::<Vec<_>>()
.join(" | ")
}
}
struct ParameterTypeUnion {
base: BaseType,
types: Vec<String>,
}
struct ParameterTypeRef {
base: BaseType,
ref_name: String,
}
impl ParameterTypeUnion {
fn parse(schema: &Value) -> Self {
let raw_types = schema
.get("type")
.and_then(Value::as_array)
.map(|arr| arr.iter().filter_map(Value::as_str).collect::<Vec<_>>())
.unwrap_or_default();
let types = raw_types
.into_iter()
.map(|t| match t {
"string" => "string".to_string(),
"number" => "number".to_string(),
"integer" => "number".to_string(),
"boolean" => "boolean".to_string(),
"null" => "null".to_string(),
"object" => "{}".to_string(),
"array" => "Array<any>".to_string(),
other => other.to_string(),
})
.collect();
Self {
base: BaseType::from_extra_props(schema, &[]),
types,
}
}
fn to_typescript(&self) -> String {
self.types.join(" | ")
}
}
impl ParameterTypeRef {
fn parse(schema: &Value, registry: &mut SchemaRegistry) -> Self {
let reference = schema.get("$ref").and_then(Value::as_str).unwrap_or("");
let resolved = registry.resolve_ref(reference);
let ref_name = match resolved {
Some(ref v) if v.get("$self_ref").and_then(Value::as_bool) == Some(true) => {
"parameters".to_string()
}
Some(_) => reference.rsplit('/').next().unwrap_or("").to_string(),
None => "any".to_string(),
};
Self {
base: BaseType::from_extra_props(schema, &[]),
ref_name,
}
}
fn to_typescript(&self) -> String {
self.ref_name.clone()
}
}
fn parse_parameter_type(schema: &Value, registry: &mut SchemaRegistry) -> ParameterType {
if registry.depth >= MAX_RECURSION_DEPTH {
return ParameterType::Scalar(ParameterTypeScalar::any());
}
registry.depth += 1;
let result = parse_parameter_type_inner(schema, registry);
registry.depth -= 1;
result
}
fn parse_parameter_type_inner(schema: &Value, registry: &mut SchemaRegistry) -> ParameterType {
if schema.is_boolean() {
return ParameterType::Scalar(ParameterTypeScalar {
base: BaseType::default(),
typ: if schema.as_bool() == Some(true) {
"any"
} else {
"null"
}
.to_string(),
});
}
let obj = match schema.as_object() {
Some(o) => o,
None => {
registry.unsupported = true;
return ParameterType::Scalar(ParameterTypeScalar::any());
}
};
if obj.contains_key("$ref") {
return ParameterType::Ref(ParameterTypeRef::parse(schema, registry));
}
if obj.contains_key("anyOf") {
return ParameterType::AnyOf(ParameterTypeAnyOf::parse(schema, registry));
}
if obj.contains_key("enum") {
return ParameterType::Enum(ParameterTypeEnum::parse(schema));
}
if let Some(typ_value) = obj.get("type") {
if typ_value.is_array() {
return ParameterType::Union(ParameterTypeUnion::parse(schema));
}
if let Some(typ) = typ_value.as_str() {
return match typ {
"object" => ParameterType::Object(ParameterTypeObject::parse(schema, registry)),
"array" => ParameterType::Array(ParameterTypeArray::parse(schema, registry)),
other => ParameterType::Scalar(ParameterTypeScalar::parse(other, schema)),
};
}
}
if obj.is_empty() {
return ParameterType::Scalar(ParameterTypeScalar::any());
}
registry.unsupported = true;
ParameterType::Scalar(ParameterTypeScalar::any())
}
fn format_description(description: &str, indent: &str) -> String {
description
.split('\n')
.map(|line| {
if line.is_empty() {
String::new()
} else {
format!("{indent}// {line}")
}
})
.collect::<Vec<_>>()
.join("\n")
}
fn json_inline(v: &Value) -> String {
match v {
Value::String(s) => s.clone(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => n.to_string(),
Value::Null => "null".to_string(),
other => serde_json::to_string(other).unwrap_or_default(),
}
}