link-assistant-router 1.4.6

Link.Assistant.Router — Claude MAX OAuth proxy and token gateway for Anthropic APIs
Documentation
fn responses_tool_choice_to_chat(choice: Option<&Value>) -> Result<Option<Value>, String> {
    let Some(choice) = choice else {
        return Ok(None);
    };
    if choice.is_string() {
        return Ok(Some(choice.clone()));
    }
    if choice.get("type").and_then(Value::as_str) == Some("function") {
        reject_unknown_fields(choice, &["type", "name"], "tool_choice")?;
        return Ok(Some(json!({
            "type": "function",
            "function": {"name": required_nonempty_string(choice, "name", "tool_choice")?}
        })));
    }
    Err("tool_choice cannot be represented by Gemini".into())
}

fn gemini_tool_choice_to_chat(config: Option<&Value>) -> Result<Option<Value>, String> {
    let Some(config) = config else {
        return Ok(None);
    };
    reject_unknown_fields(
        config,
        &["functionCallingConfig", "function_calling_config"],
        "toolConfig",
    )?;
    let calling = config
        .get("functionCallingConfig")
        .or_else(|| config.get("function_calling_config"))
        .ok_or_else(|| "toolConfig.functionCallingConfig is required".to_string())?;
    reject_unknown_fields(
        calling,
        &["mode", "allowedFunctionNames", "allowed_function_names"],
        "toolConfig.functionCallingConfig",
    )?;
    let mode = required_nonempty_string(calling, "mode", "toolConfig")?.to_ascii_uppercase();
    let allowed = calling
        .get("allowedFunctionNames")
        .or_else(|| calling.get("allowed_function_names"));
    match (mode.as_str(), allowed) {
        ("AUTO", None) => Ok(Some(json!("auto"))),
        ("NONE", None) => Ok(Some(json!("none"))),
        ("ANY", None) => Ok(Some(json!("required"))),
        ("ANY", Some(Value::Array(names))) if names.len() == 1 => Ok(Some(json!({
            "type": "function",
            "function": {"name": names[0].as_str().ok_or_else(|| {
                "toolConfig.allowedFunctionNames must contain strings".to_string()
            })?}
        }))),
        _ => Err("toolConfig function-calling policy cannot be represented exactly".into()),
    }
}

fn responses_content_to_chat(
    content: Option<&Value>,
    role: &str,
    path: &str,
) -> Result<Value, String> {
    match content {
        Some(Value::String(text)) => Ok(Value::String(text.clone())),
        Some(Value::Array(parts)) => parts
            .iter()
            .enumerate()
            .map(|(index, part)| {
                let path = format!("{path}.content[{index}]");
                match part.get("type").and_then(Value::as_str) {
                    Some("input_text" | "output_text" | "text") => {
                        reject_unknown_fields(part, &["type", "text"], &path)?;
                        Ok(json!({
                            "type": "text", "text": required_string(part, "text", &path)?
                        }))
                    }
                    Some("input_image") if role == "user" => {
                        reject_unknown_fields(
                            part,
                            &["type", "image_url", "file_id", "detail"],
                            &path,
                        )?;
                        if part.get("file_id").is_some_and(|value| !value.is_null()) {
                            return Err(format!("{path}.file_id is provider-specific"));
                        }
                        reject_image_detail(part.get("detail"), &path)?;
                        Ok(json!({
                            "type": "image_url",
                            "image_url": {"url": required_nonempty_string(
                                part, "image_url", &path
                            )?}
                        }))
                    }
                    Some(kind) => Err(format!(
                        "{path} content type {kind} cannot be represented by Gemini"
                    )),
                    None => Err(format!("{path} is missing a string type")),
                }
            })
            .collect::<Result<Vec<_>, _>>()
            .map(Value::Array),
        _ => Err(format!("{path}.content must be a string or array")),
    }
}

fn responses_tool_output(output: Option<&Value>, path: &str) -> Result<Value, String> {
    match output {
        Some(Value::String(text)) => Ok(Value::String(text.clone())),
        Some(Value::Array(parts)) => {
            let mut text = String::new();
            for (index, part) in parts.iter().enumerate() {
                reject_unknown_fields(part, &["type", "text"], &format!("{path}.output[{index}]"))?;
                if part.get("type").and_then(Value::as_str) != Some("input_text") {
                    return Err(format!(
                        "{path}.output[{index}] cannot be represented in a Gemini function response"
                    ));
                }
                text.push_str(required_string(part, "text", path)?);
            }
            Ok(Value::String(text))
        }
        _ => Err(format!(
            "{path}.output must be a string or input_text array"
        )),
    }
}

fn tool_response(content: Option<&Value>, path: &str) -> Result<Value, String> {
    let text = match content {
        Some(Value::String(text)) => text.clone(),
        Some(Value::Array(parts)) => {
            let mut text = String::new();
            for (index, part) in parts.iter().enumerate() {
                reject_unknown_fields(
                    part,
                    &["type", "text"],
                    &format!("{path}.content[{index}]"),
                )?;
                if part.get("type").and_then(Value::as_str) != Some("text") {
                    return Err(format!("{path}.content[{index}] is not a text tool result"));
                }
                text.push_str(required_string(part, "text", path)?);
            }
            text
        }
        _ => return Err(format!("{path}.content must be text")),
    };
    Ok(serde_json::from_str(&text)
        .ok()
        .filter(Value::is_object)
        .unwrap_or_else(|| json!({"output": text})))
}

fn parse_image_data_url<'a>(url: &'a str, path: &str) -> Result<(&'a str, &'a str), String> {
    let encoded = url
        .strip_prefix("data:")
        .ok_or_else(|| format!("{path} must be a data URL"))?;
    let (media, data) = encoded
        .split_once(";base64,")
        .ok_or_else(|| format!("{path} must be a base64 data URL"))?;
    validate_image_media(media, path)?;
    if data.is_empty() || decode_base64(data).is_err() {
        return Err(format!("{path} contains invalid base64 image data"));
    }
    Ok((media, data))
}