use std::collections::HashMap;
use std::sync::Arc;
use tensor_wasm_exec::executor::WasmArg;
use uuid::Uuid;
use crate::openai::{ChatCompletionsRequest, ChatMessage, CompletionsRequest, OpenAiError};
pub const ENV_OPENAI_MODEL_MAP: &str = "TENSOR_WASM_API_OPENAI_MODEL_MAP";
pub type ModelMap = Arc<HashMap<String, Uuid>>;
pub fn parse_model_map_env(raw: &str) -> HashMap<String, Uuid> {
let mut out: HashMap<String, Uuid> = HashMap::new();
let trimmed = raw.trim();
if trimmed.is_empty() {
return out;
}
for entry in trimmed.split(',') {
let frag = entry.trim();
if frag.is_empty() {
continue;
}
let (model, uuid_str) = match frag.split_once(':') {
Some((m, u)) => (m.trim(), u.trim()),
None => {
tracing::warn!(
target: "tensor_wasm_api::openai_translator",
entry = %frag,
var = ENV_OPENAI_MODEL_MAP,
"ignoring malformed entry: missing ':' separator (expected model:function_uuid)",
);
continue;
}
};
if model.is_empty() {
tracing::warn!(
target: "tensor_wasm_api::openai_translator",
entry = %frag,
var = ENV_OPENAI_MODEL_MAP,
"ignoring malformed entry: empty model id",
);
continue;
}
let id = match Uuid::parse_str(uuid_str) {
Ok(u) => u,
Err(e) => {
tracing::warn!(
target: "tensor_wasm_api::openai_translator",
entry = %frag,
var = ENV_OPENAI_MODEL_MAP,
error = %e,
"ignoring malformed entry: function uuid does not parse",
);
continue;
}
};
out.insert(model.to_owned(), id);
}
out
}
pub fn model_map_from_env() -> ModelMap {
let raw = std::env::var(ENV_OPENAI_MODEL_MAP).unwrap_or_default();
let map = parse_model_map_env(&raw);
if !map.is_empty() {
tracing::info!(
target: "tensor_wasm_api::openai_translator",
entries = map.len(),
"OpenAI model map configured",
);
}
Arc::new(map)
}
#[derive(Debug, Clone)]
pub struct TranslatedRequest {
pub function_id: Uuid,
pub prompt: String,
pub prompt_len_hint: i32,
pub args: Vec<WasmArg>,
pub stream: bool,
}
fn extract_prompt_text(prompt: &serde_json::Value) -> String {
match prompt {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Array(items) => items
.iter()
.filter_map(|v| v.as_str().map(str::to_owned))
.collect::<Vec<_>>()
.join("\n"),
_ => String::new(),
}
}
fn extract_message_content(msg: &ChatMessage) -> String {
match &msg.content {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Array(parts) => parts
.iter()
.filter_map(|p| {
p.get("text")
.and_then(serde_json::Value::as_str)
.map(str::to_owned)
})
.collect::<Vec<_>>()
.join(" "),
_ => String::new(),
}
}
pub fn assemble_chat_prompt(messages: &[ChatMessage]) -> String {
let mut out = String::new();
for msg in messages {
let role = if msg.role.is_empty() {
"user"
} else {
msg.role.as_str()
};
let content = extract_message_content(msg);
out.push_str(role);
out.push_str(": ");
out.push_str(&content);
out.push('\n');
}
out.push_str("assistant:");
out
}
pub fn translate_completions_request(
req: &CompletionsRequest,
model_map: &HashMap<String, Uuid>,
) -> Result<TranslatedRequest, OpenAiError> {
let function_id = lookup_model(&req.model, model_map)?;
reject_unsupported_knob("max_tokens", req.max_tokens.map(|v| v as f64), None)?;
reject_unsupported_knob("temperature", req.temperature.map(|v| v as f64), Some(1.0))?;
reject_unsupported_knob("n", req.n.map(|v| v as f64), Some(1.0))?;
reject_unsupported_bool("echo", req.echo)?;
let prompt = extract_prompt_text(&req.prompt);
let prompt_len_hint = clamp_len_to_i32(prompt.len());
Ok(TranslatedRequest {
function_id,
prompt,
prompt_len_hint,
args: Vec::new(),
stream: req.stream.unwrap_or(false),
})
}
pub fn translate_chat_completions_request(
req: &ChatCompletionsRequest,
model_map: &HashMap<String, Uuid>,
) -> Result<TranslatedRequest, OpenAiError> {
let function_id = lookup_model(&req.model, model_map)?;
reject_unsupported_knob("max_tokens", req.max_tokens.map(|v| v as f64), None)?;
reject_unsupported_knob("temperature", req.temperature.map(|v| v as f64), Some(1.0))?;
reject_unsupported_knob("n", req.n.map(|v| v as f64), Some(1.0))?;
reject_unsupported_tools(req.tools.as_ref())?;
let prompt = assemble_chat_prompt(&req.messages);
let prompt_len_hint = clamp_len_to_i32(prompt.len());
Ok(TranslatedRequest {
function_id,
prompt,
prompt_len_hint,
args: Vec::new(),
stream: req.stream.unwrap_or(false),
})
}
fn lookup_model(model: &str, model_map: &HashMap<String, Uuid>) -> Result<Uuid, OpenAiError> {
match model_map.get(model) {
Some(id) => Ok(*id),
None => Err(OpenAiError::model_not_found(format!(
"model `{model}` is not configured in TENSOR_WASM_API_OPENAI_MODEL_MAP; \
ask your operator to add a `{model}:<function_uuid>` entry",
))),
}
}
#[allow(clippy::float_cmp)]
fn reject_unsupported_knob(
field: &'static str,
value: Option<f64>,
honored_default: Option<f64>,
) -> Result<(), OpenAiError> {
let Some(v) = value else {
return Ok(());
};
if Some(v) == honored_default {
return Ok(());
}
Err(OpenAiError::invalid_request(
format!(
"`{field}` is not supported by this gateway: the underlying executor exposes no \
sampling controls. Omit `{field}` and retry.",
),
Some(field.to_string()),
)
.with_code("unsupported_parameter"))
}
fn reject_unsupported_bool(field: &'static str, value: Option<bool>) -> Result<(), OpenAiError> {
if value == Some(true) {
return Err(OpenAiError::invalid_request(
format!(
"`{field}` is not supported by this gateway: the underlying executor cannot \
echo the prompt. Omit `{field}` (or send `{field}: false`) and retry.",
),
Some(field.to_string()),
)
.with_code("unsupported_parameter"));
}
Ok(())
}
fn reject_unsupported_tools(tools: Option<&serde_json::Value>) -> Result<(), OpenAiError> {
let populated = match tools {
None | Some(serde_json::Value::Null) => false,
Some(serde_json::Value::Array(a)) => !a.is_empty(),
Some(_) => true,
};
if populated {
return Err(OpenAiError::invalid_request(
"`tools` is not supported by this gateway: the underlying executor has no \
tool-calling dispatch. Omit `tools` and retry."
.to_string(),
Some("tools".to_string()),
)
.with_code("unsupported_parameter"));
}
Ok(())
}
fn clamp_len_to_i32(len: usize) -> i32 {
if len > i32::MAX as usize {
i32::MAX
} else {
len as i32
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn fixture_uuid_1() -> Uuid {
Uuid::parse_str("00000000-0000-4000-8000-000000000001").unwrap()
}
fn fixture_uuid_2() -> Uuid {
Uuid::parse_str("00000000-0000-4000-8000-000000000002").unwrap()
}
#[test]
fn parse_empty_yields_empty_map() {
assert!(parse_model_map_env("").is_empty());
assert!(parse_model_map_env(" ").is_empty());
}
#[test]
fn parse_single_entry() {
let raw = "gpt-3.5-turbo:00000000-0000-4000-8000-000000000001";
let map = parse_model_map_env(raw);
assert_eq!(map.len(), 1);
assert_eq!(map.get("gpt-3.5-turbo"), Some(&fixture_uuid_1()));
}
#[test]
fn parse_multiple_entries() {
let raw = "gpt-3.5-turbo:00000000-0000-4000-8000-000000000001,gpt-4:00000000-0000-4000-8000-000000000002";
let map = parse_model_map_env(raw);
assert_eq!(map.len(), 2);
assert_eq!(map.get("gpt-3.5-turbo"), Some(&fixture_uuid_1()));
assert_eq!(map.get("gpt-4"), Some(&fixture_uuid_2()));
}
#[test]
fn parse_skips_malformed_entries() {
let raw = "good:00000000-0000-4000-8000-000000000001,bad-no-colon,empty:,:no-model,gpt-4:not-a-uuid";
let map = parse_model_map_env(raw);
assert_eq!(map.len(), 1);
assert!(map.contains_key("good"));
}
#[test]
fn parse_tolerates_whitespace() {
let raw = " gpt-3.5-turbo : 00000000-0000-4000-8000-000000000001 , gpt-4:00000000-0000-4000-8000-000000000002 ";
let map = parse_model_map_env(raw);
assert_eq!(map.len(), 2);
assert!(map.contains_key("gpt-3.5-turbo"));
assert!(map.contains_key("gpt-4"));
}
#[test]
fn translate_completions_returns_function_id_on_hit() {
let mut map = HashMap::new();
map.insert("m1".to_owned(), fixture_uuid_1());
let req = CompletionsRequest {
model: "m1".to_owned(),
prompt: json!("hello"),
stream: Some(false),
..Default::default()
};
let out = translate_completions_request(&req, &map).expect("translates");
assert_eq!(out.function_id, fixture_uuid_1());
assert_eq!(out.prompt, "hello");
assert!(!out.stream);
assert!(out.args.is_empty(), "args must be empty in v0.4");
assert_eq!(out.prompt_len_hint, 5);
}
#[test]
fn translate_completions_rejects_unsupported_knobs() {
let mut map = HashMap::new();
map.insert("m1".to_owned(), fixture_uuid_1());
let base = CompletionsRequest {
model: "m1".to_owned(),
prompt: json!("hi"),
..Default::default()
};
let req = CompletionsRequest {
max_tokens: Some(64),
..base.clone()
};
let err = translate_completions_request(&req, &map).expect_err("rejects max_tokens");
assert_eq!(err.error.code.as_deref(), Some("unsupported_parameter"));
assert_eq!(err.error.param.as_deref(), Some("max_tokens"));
let req = CompletionsRequest {
temperature: Some(0.7),
..base.clone()
};
assert!(translate_completions_request(&req, &map).is_err());
let req = CompletionsRequest {
echo: Some(true),
..base.clone()
};
assert!(translate_completions_request(&req, &map).is_err());
let req = CompletionsRequest {
echo: Some(false),
..base.clone()
};
assert!(translate_completions_request(&req, &map).is_ok());
let req = CompletionsRequest {
n: Some(1),
..base.clone()
};
assert!(translate_completions_request(&req, &map).is_ok());
let req = CompletionsRequest { n: Some(2), ..base };
assert!(translate_completions_request(&req, &map).is_err());
}
#[test]
fn translate_chat_completions_rejects_nonempty_tools() {
let mut map = HashMap::new();
map.insert("m".to_owned(), fixture_uuid_1());
let base = ChatCompletionsRequest {
model: "m".to_owned(),
messages: vec![ChatMessage {
role: "user".to_owned(),
content: json!("hi"),
name: None,
}],
..Default::default()
};
let req = ChatCompletionsRequest {
tools: Some(json!([{"type": "function"}])),
..base.clone()
};
let err = translate_chat_completions_request(&req, &map).expect_err("rejects tools");
assert_eq!(err.error.code.as_deref(), Some("unsupported_parameter"));
assert_eq!(err.error.param.as_deref(), Some("tools"));
let req = ChatCompletionsRequest {
tools: Some(json!([])),
..base.clone()
};
assert!(translate_chat_completions_request(&req, &map).is_ok());
let req = ChatCompletionsRequest {
tools: Some(serde_json::Value::Null),
..base
};
assert!(translate_chat_completions_request(&req, &map).is_ok());
}
#[test]
fn translate_completions_returns_model_not_found_on_miss() {
let map = HashMap::new();
let req = CompletionsRequest {
model: "unknown".to_owned(),
prompt: json!("hello"),
..Default::default()
};
let err = translate_completions_request(&req, &map).expect_err("misses");
assert_eq!(err.error.code.as_deref(), Some("model_not_found"));
}
#[test]
fn translate_completions_array_prompt_joined_with_newlines() {
let mut map = HashMap::new();
map.insert("m1".to_owned(), fixture_uuid_1());
let req = CompletionsRequest {
model: "m1".to_owned(),
prompt: json!(["a", "b", "c"]),
..Default::default()
};
let out = translate_completions_request(&req, &map).expect("translates");
assert_eq!(out.prompt, "a\nb\nc");
}
#[test]
fn translate_chat_completions_assembles_role_tagged_prompt() {
let mut map = HashMap::new();
map.insert("m2".to_owned(), fixture_uuid_2());
let req = ChatCompletionsRequest {
model: "m2".to_owned(),
messages: vec![
ChatMessage {
role: "system".to_owned(),
content: json!("You are helpful."),
name: None,
},
ChatMessage {
role: "user".to_owned(),
content: json!("Hi."),
name: None,
},
],
stream: Some(true),
..Default::default()
};
let out = translate_chat_completions_request(&req, &map).expect("translates");
assert_eq!(out.function_id, fixture_uuid_2());
assert!(out.stream);
assert!(
out.prompt.contains("system: You are helpful."),
"missing system role tag: {}",
out.prompt,
);
assert!(
out.prompt.contains("user: Hi."),
"missing user role tag: {}",
out.prompt,
);
assert!(
out.prompt.trim_end().ends_with("assistant:"),
"must end with trailing `assistant:` turn marker: {}",
out.prompt,
);
}
#[test]
fn translate_chat_completions_empty_messages_yields_only_marker() {
let mut map = HashMap::new();
map.insert("m".to_owned(), fixture_uuid_1());
let req = ChatCompletionsRequest {
model: "m".to_owned(),
messages: vec![],
..Default::default()
};
let out = translate_chat_completions_request(&req, &map).expect("translates");
assert_eq!(out.prompt, "assistant:");
}
#[test]
fn translate_chat_completions_default_role_is_user() {
let mut map = HashMap::new();
map.insert("m".to_owned(), fixture_uuid_1());
let req = ChatCompletionsRequest {
model: "m".to_owned(),
messages: vec![ChatMessage {
role: String::new(),
content: json!("hi"),
name: None,
}],
..Default::default()
};
let out = translate_chat_completions_request(&req, &map).expect("translates");
assert!(out.prompt.contains("user: hi"), "got {}", out.prompt);
}
}