Skip to main content

llmsdk_provider/
skills_model.rs

1//! Skill upload model trait and supporting types.
2//!
3//! Mirrors `@ai-sdk/provider/src/skills/v4/*`. A "skill" is a bundle of files
4//! grouped under a single identifier; the only current provider is
5//! `Anthropic` (`POST /v1/skills`, beta `skills-2025-10-02`), where the
6//! returned skill id can be referenced from the Messages API
7//! `container.skills[]` field.
8//!
9//! Kept separate from [`crate::Provider`] for the same reason as
10//! [`crate::FilesModel`]: only providers that expose a skills endpoint
11//! implement this trait.
12// Rust guideline compliant 2026-02-21
13
14use async_trait::async_trait;
15use serde::{Deserialize, Serialize};
16
17use crate::error::Result;
18use crate::files_model::UploadFileData;
19use crate::shared::{ProviderMetadata, ProviderOptions, ProviderReference, Warning};
20
21/// Contract every skill-upload model implements.
22///
23/// Mirrors `SkillsV4`.
24#[async_trait]
25pub trait SkillsModel: Send + Sync + std::fmt::Debug {
26    /// Provider id, e.g. `"anthropic.skills"`.
27    fn provider(&self) -> &str;
28
29    /// Specification version (currently `"v4"`).
30    fn specification_version(&self) -> &'static str {
31        "v4"
32    }
33
34    /// Upload a skill from the given files.
35    ///
36    /// # Errors
37    ///
38    /// Returns a [`crate::ProviderError`] when the upstream call fails or
39    /// the response is malformed.
40    async fn upload_skill(&self, options: UploadSkillOptions) -> Result<UploadSkillResult>;
41}
42
43/// One file within a skill bundle.
44///
45/// Mirrors `SkillsV4File`.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct SkillFile {
48    /// Path of the file relative to the skill root (e.g. `"main.py"`).
49    pub path: String,
50    /// File payload.
51    pub data: UploadFileData,
52}
53
54/// Options for one [`SkillsModel::upload_skill`] call.
55///
56/// Mirrors `SkillsV4UploadSkillCallOptions`.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct UploadSkillOptions {
59    /// Files that make up the skill (at least one).
60    pub files: Vec<SkillFile>,
61    /// Optional human-readable title.
62    #[serde(
63        default,
64        rename = "displayTitle",
65        skip_serializing_if = "Option::is_none"
66    )]
67    pub display_title: Option<String>,
68    /// Provider-specific options.
69    #[serde(
70        default,
71        rename = "providerOptions",
72        skip_serializing_if = "Option::is_none"
73    )]
74    pub provider_options: Option<ProviderOptions>,
75}
76
77/// Result of [`SkillsModel::upload_skill`].
78///
79/// Mirrors `SkillsV4UploadSkillResult`.
80#[derive(Debug, Clone)]
81pub struct UploadSkillResult {
82    /// `{ providerId → skillId }` reference.
83    pub provider_reference: ProviderReference,
84    /// Display title as stored by the provider.
85    pub display_title: Option<String>,
86    /// Skill name (often resolved from the latest version manifest).
87    pub name: Option<String>,
88    /// Skill description.
89    pub description: Option<String>,
90    /// Latest version identifier reported by the provider.
91    pub latest_version: Option<String>,
92    /// Provider-specific metadata.
93    pub provider_metadata: Option<ProviderMetadata>,
94    /// Warnings (e.g. setting coerced away).
95    pub warnings: Vec<Warning>,
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::shared::FileBytes;
102
103    #[test]
104    fn options_serde_roundtrip() {
105        let opts = UploadSkillOptions {
106            files: vec![SkillFile {
107                path: "main.py".into(),
108                data: UploadFileData::Text {
109                    text: "print(1)".into(),
110                },
111            }],
112            display_title: Some("greeter".into()),
113            provider_options: None,
114        };
115        let json = serde_json::to_value(&opts).unwrap();
116        assert_eq!(json["displayTitle"], "greeter");
117        assert_eq!(json["files"][0]["path"], "main.py");
118        assert_eq!(json["files"][0]["data"]["type"], "text");
119    }
120
121    #[test]
122    fn skill_file_supports_bytes() {
123        let f = SkillFile {
124            path: "asset.bin".into(),
125            data: UploadFileData::Data {
126                data: FileBytes::Bytes(vec![1, 2, 3]),
127            },
128        };
129        let v = serde_json::to_value(&f).unwrap();
130        assert_eq!(v["data"]["type"], "data");
131    }
132}