import re
from pathlib import Path
RUST_TO_TS = {
"String": "string",
"str": "string",
"bool": "boolean",
"i8": "number", "i16": "number", "i32": "number", "i64": "number",
"u8": "number", "u16": "number", "u32": "number", "u64": "number",
"f32": "number", "f64": "number",
"serde_json::Value": "any",
"serde_json::Map<String, serde_json::Value>": "Record<string, any>",
"Vec<u8>": "Uint8Array",
}
def rust_type_to_ts(rust_type: str) -> str:
rust_type = rust_type.strip().rstrip(",")
m = re.match(r"Option<(.+)>", rust_type)
if m:
inner = rust_type_to_ts(m.group(1))
return f"{inner} | undefined"
m = re.match(r"Vec<(.+)>", rust_type)
if m:
inner = rust_type_to_ts(m.group(1))
return f"{inner}[]"
m = re.match(r"(?:std::collections::)?HashMap<(.+),\s*(.+)>", rust_type)
if m:
k = rust_type_to_ts(m.group(1))
v = rust_type_to_ts(m.group(2))
return f"Record<{k}, {v}>"
if rust_type in RUST_TO_TS:
return RUST_TO_TS[rust_type]
if rust_type and rust_type[0].isupper():
return rust_type
return "any"
def parse_struct(name: str, body: str) -> list[tuple[str, str, bool]]:
fields = []
for line in body.split("\n"):
line = line.strip()
if not line.startswith("pub "):
continue
m = re.match(r"pub (\w+): (.+)", line)
if not m:
continue
field_name = m.group(1)
rust_type = m.group(2).rstrip(",").strip()
optional = rust_type.startswith("Option<")
ts_type = rust_type_to_ts(rust_type)
ts_name = to_camel_case(field_name) if field_name != field_name.lower() else field_name
ts_name = field_name
fields.append((ts_name, ts_type, optional))
return fields
def parse_enum_variants(body: str) -> list[str]:
values = []
for line in body.split("\n"):
line = line.strip()
m = re.search(r'#\[serde\(rename\s*=\s*"([^"]+)"\)', line)
if m:
values.append(m.group(1))
continue
m = re.match(r"^(\w+),?$", line)
if m and m.group(1) not in ("Other",):
variant = m.group(1)
snake = re.sub(r"([A-Z])", r"_\1", variant).lower().strip("_")
values.append(snake)
return values
def to_camel_case(s: str) -> str:
parts = s.split("_")
return parts[0] + "".join(p.capitalize() for p in parts[1:])
def generate_types(rust_root: Path) -> str:
lines = [
"// Auto-generated TypeScript types from openai-types Rust crate.",
"// Do not edit manually. Regenerate: python3 scripts/gen_ts_types.py",
"//",
f"// Generated from {sum(1 for _ in rust_root.rglob('*.rs'))} Rust source files.",
"",
"export namespace OpenAI {",
]
for domain_dir in sorted(rust_root.iterdir()):
if not domain_dir.is_dir() or domain_dir.name.startswith("_"):
continue
domain_name = domain_dir.name
structs = []
enums = []
for rs_file in sorted(domain_dir.glob("*.rs")):
if rs_file.stem == "mod":
continue
text = rs_file.read_text()
for m in re.finditer(
r"pub struct (\w+)\s*\{([^}]*)\}", text, re.DOTALL
):
name = m.group(1)
fields = parse_struct(name, m.group(2))
if fields:
structs.append((name, fields))
for m in re.finditer(
r"pub enum (\w+)\s*\{([^}]*)\}", text, re.DOTALL
):
name = m.group(1)
variants = parse_enum_variants(m.group(2))
if variants:
enums.append((name, variants))
if not structs and not enums:
continue
lines.append(f" export namespace {domain_name} {{")
for name, variants in enums:
seen = []
for v in variants:
if v not in seen:
seen.append(v)
variants = seen
quoted = " | ".join(f'"{v}"' for v in variants)
lines.append(f" export type {name} = {quoted};")
if enums and structs:
lines.append("")
for name, fields in structs:
lines.append(f" export interface {name} {{")
for ts_name, ts_type, optional in fields:
opt = "?" if optional else ""
lines.append(f" {ts_name}{opt}: {ts_type};")
lines.append(" }")
lines.append("")
lines.append(" }")
lines.append("")
lines.append("}")
lines.append("")
lines.extend([
"// Convenience re-exports for common types",
"export type ChatCompletionRequest = OpenAI.chat.ChatCompletionRequest;",
"export type ChatCompletionResponse = OpenAI.chat.ChatCompletionResponse;",
"export type ResponseCreateRequest = OpenAI.responses.ResponseCreateRequest;",
"export type Response = OpenAI.responses.Response;",
"",
])
return "\n".join(lines)
def main():
rust_root = Path("openai-types/src")
output = Path("openai-oxide-node/types.d.ts")
ts = generate_types(rust_root)
output.write_text(ts)
interfaces = ts.count("export interface")
type_aliases = ts.count("export type")
print(f"Generated {output}: {interfaces} interfaces, {type_aliases} type aliases")
if __name__ == "__main__":
main()