From cfde1212db753ccc6f5175ca44c086007466f290 Mon Sep 17 00:00:00 2001 From: Aliaksandr Stsiapanay Date: Thu, 10 Sep 2026 16:03:31 +0300 Subject: [PATCH 1/2] feat: extend unified chat completion request with custom skills #1955 Adds messages[*].custom_content.skills[*] so callers can reference existing Skill resources per-request, auto-shared into ApiKeyData.attachedSkills the same way file attachments already are. --- docs/open_api_core.yaml | 18 +++ .../controller/DeploymentPostController.java | 2 + .../function/CollectRequestSkillsFn.java | 48 +++++++ .../request/ChatCompletionRequest.java | 5 + .../function/request/RequestObject.java | 9 ++ .../aidial/core/server/util/ChatUtil.java | 15 +++ .../DeploymentPostControllerTest.java | 123 ++++++++++++++++++ .../function/CollectRequestSkillsFnTest.java | 116 +++++++++++++++++ .../request/ChatCompletionRequestTest.java | 99 ++++++++++++++ 9 files changed, 435 insertions(+) create mode 100644 server/src/main/java/com/epam/aidial/core/server/function/CollectRequestSkillsFn.java create mode 100644 server/src/test/java/com/epam/aidial/core/server/function/CollectRequestSkillsFnTest.java diff --git a/docs/open_api_core.yaml b/docs/open_api_core.yaml index 4ea0ec37a..0c24dd71d 100644 --- a/docs/open_api_core.yaml +++ b/docs/open_api_core.yaml @@ -14762,6 +14762,11 @@ components: description: |- The JSON schema describing a form that the assistant prompts the user to fill in. Given this schema, the user is expected to provide a JSON value in the next message in the `custom_content.form_value` field. + skills: + type: array + description: List of skills referenced by the model as part of the response. + items: + $ref: "#/components/schemas/RequestSkill" description: The custom content of the assistant message. ChatCompletionRequestCustomFields: type: object @@ -15005,6 +15010,11 @@ components: form_value: type: object description: The JSON value corresponding to the JSON schema sent by the assistant in the previous message in the `custom_content.form_schema` field. + skills: + type: array + description: List of skills used to supply an additional input for the model. + items: + $ref: "#/components/schemas/RequestSkill" description: The custom content of the user message. ChatCompletionResponseAttachment: required: @@ -16961,6 +16971,14 @@ components: reference_url: type: string description: "If `reference_type` is specified, the content of `reference_url` should follow the format described in the MIME standard for `reference_type`." + RequestSkill: + type: object + required: + - url + properties: + url: + type: string + description: The URL of the skill resource, e.g. `skills//`. ResourceAccessType: type: string enum: diff --git a/server/src/main/java/com/epam/aidial/core/server/controller/DeploymentPostController.java b/server/src/main/java/com/epam/aidial/core/server/controller/DeploymentPostController.java index 47525eb0f..492585065 100644 --- a/server/src/main/java/com/epam/aidial/core/server/controller/DeploymentPostController.java +++ b/server/src/main/java/com/epam/aidial/core/server/controller/DeploymentPostController.java @@ -22,6 +22,7 @@ import com.epam.aidial.core.server.function.CollectChatCompletionUsageFn; import com.epam.aidial.core.server.function.CollectDeploymentsFn; import com.epam.aidial.core.server.function.CollectRequestApplicationFilesFn; +import com.epam.aidial.core.server.function.CollectRequestSkillsFn; import com.epam.aidial.core.server.function.CollectRequestStandardAttachmentsFn; import com.epam.aidial.core.server.function.CollectResponseChatCompletionAttachmentsFn; import com.epam.aidial.core.server.function.StripUsagePerModelFn; @@ -74,6 +75,7 @@ public DeploymentPostController(Proxy proxy, ProxyContext context) { */ private List> buildEnhancementFunctions() { return List.of(new CollectRequestStandardAttachmentsFn(proxy, context), + new CollectRequestSkillsFn(proxy, context), new ApplyDefaultDeploymentSettingsFn(proxy, context, requestedInterface()), new EnhanceDeploymentRequestFn(proxy, context), new CollectRequestApplicationFilesFn(proxy, context), diff --git a/server/src/main/java/com/epam/aidial/core/server/function/CollectRequestSkillsFn.java b/server/src/main/java/com/epam/aidial/core/server/function/CollectRequestSkillsFn.java new file mode 100644 index 000000000..2f3aa5182 --- /dev/null +++ b/server/src/main/java/com/epam/aidial/core/server/function/CollectRequestSkillsFn.java @@ -0,0 +1,48 @@ +package com.epam.aidial.core.server.function; + +import com.epam.aidial.core.config.ResourceAccessType; +import com.epam.aidial.core.server.Proxy; +import com.epam.aidial.core.server.ProxyContext; +import com.epam.aidial.core.server.data.AutoSharedData; +import com.epam.aidial.core.server.function.request.RequestObject; +import com.epam.aidial.core.server.security.AccessService; +import com.epam.aidial.core.storage.http.HttpException; +import com.epam.aidial.core.storage.http.HttpStatus; +import com.epam.aidial.core.storage.resource.ResourceDescriptor; +import com.epam.aidial.core.storage.resource.ResourceTypes; + +/** + * Collects skill resources referenced from {@code messages[*].custom_content.skills[*]} and auto-shares + * them to the invoked deployment's per-request API key, the same way {@link CollectRequestAttachmentsFn} + * auto-shares attachments. + */ +public class CollectRequestSkillsFn extends BaseRequestFunction { + public CollectRequestSkillsFn(Proxy proxy, ProxyContext context) { + super(proxy, context); + } + + @Override + public Boolean apply(RequestObject request) { + for (String url : request.collectSkills()) { + tryToAutoShareAttachedSkill(url); + } + return false; + } + + private void tryToAutoShareAttachedSkill(String url) { + ResourceDescriptor resource = fromAnyUrl(url, proxy.getEncryptionService()); + if (resource == null || resource.getType() != ResourceTypes.SKILL) { + throw new HttpException(HttpStatus.BAD_REQUEST, "Url must reference a skill resource: %s".formatted(url)); + } + if (resource.isPublic()) { + return; + } + AccessService accessService = proxy.getAccessService(); + if (accessService.hasReadAccess(resource, context)) { + context.getProxyApiKeyData().getAttachedSkills() + .put(resource.getUrl(), new AutoSharedData(ResourceAccessType.READ_ONLY)); + } else { + throw new HttpException(HttpStatus.FORBIDDEN, "Access denied to the skill %s".formatted(url)); + } + } +} diff --git a/server/src/main/java/com/epam/aidial/core/server/function/request/ChatCompletionRequest.java b/server/src/main/java/com/epam/aidial/core/server/function/request/ChatCompletionRequest.java index 106292386..8747f30e8 100644 --- a/server/src/main/java/com/epam/aidial/core/server/function/request/ChatCompletionRequest.java +++ b/server/src/main/java/com/epam/aidial/core/server/function/request/ChatCompletionRequest.java @@ -60,6 +60,11 @@ public Set collectAppAttachments(List paths) { return ChatUtil.collectAttachments(tree, paths); } + @Override + public Set collectSkills() { + return ChatUtil.collectCustomSkills(tree, List.of("$.messages[*].custom_content.skills[*]")); + } + @Override public List buildCacheKeys(List nodeOrder) { CacheKeyBuilder builder = new CacheKeyBuilder(); diff --git a/server/src/main/java/com/epam/aidial/core/server/function/request/RequestObject.java b/server/src/main/java/com/epam/aidial/core/server/function/request/RequestObject.java index 83f8e4740..f4d75f7f8 100644 --- a/server/src/main/java/com/epam/aidial/core/server/function/request/RequestObject.java +++ b/server/src/main/java/com/epam/aidial/core/server/function/request/RequestObject.java @@ -38,6 +38,15 @@ public interface RequestObject { */ Set collectAttachments(); + /** + * Collects skill resource URLs present in the request body. + * + * @return a set of skill URLs found in the request body, or an empty set if this request shape doesn't support it + */ + default Set collectSkills() { + return Set.of(); + } + /** * Collects image and file URLs located at the specified paths. * diff --git a/server/src/main/java/com/epam/aidial/core/server/util/ChatUtil.java b/server/src/main/java/com/epam/aidial/core/server/util/ChatUtil.java index 7678c7f6f..2d97beba2 100644 --- a/server/src/main/java/com/epam/aidial/core/server/util/ChatUtil.java +++ b/server/src/main/java/com/epam/aidial/core/server/util/ChatUtil.java @@ -22,6 +22,10 @@ public Set collectCustomAttachments(JsonNode node, List paths) { return JsonUtil.collectStrings(node, paths, ChatUtil::readCustomAttachment); } + public Set collectCustomSkills(JsonNode node, List paths) { + return JsonUtil.collectStrings(node, paths, ChatUtil::readCustomSkill); + } + private String readAttachment(JsonNode node) { if (!node.isTextual()) { throw new IllegalArgumentException("Invalid attachment."); @@ -49,6 +53,17 @@ public String readCustomAttachment(JsonNode node) { return url; } + public String readCustomSkill(JsonNode node) { + if (!node.isObject()) { + throw new IllegalArgumentException("Invalid skill attachment."); + } + String url = node.path("url").asText(); + if (StringUtils.isBlank(url)) { + throw new IllegalArgumentException("Missing url in skill attachment."); + } + return url; + } + public void removeInterceptorConfiguration(ObjectNode node) { ObjectNode customFields = (ObjectNode) node.get(CUSTOM_FIELDS_NODE); if (customFields != null) { diff --git a/server/src/test/java/com/epam/aidial/core/server/controller/DeploymentPostControllerTest.java b/server/src/test/java/com/epam/aidial/core/server/controller/DeploymentPostControllerTest.java index 75335ef6b..3ca77b6ad 100644 --- a/server/src/test/java/com/epam/aidial/core/server/controller/DeploymentPostControllerTest.java +++ b/server/src/test/java/com/epam/aidial/core/server/controller/DeploymentPostControllerTest.java @@ -70,6 +70,7 @@ import static com.epam.aidial.core.server.Proxy.HEADER_APPLICATION_PROPERTIES; import static com.epam.aidial.core.server.Proxy.HEADER_CONTENT_TYPE_APPLICATION_JSON; import static com.epam.aidial.core.storage.http.HttpStatus.BAD_GATEWAY; +import static com.epam.aidial.core.storage.http.HttpStatus.BAD_REQUEST; import static com.epam.aidial.core.storage.http.HttpStatus.FORBIDDEN; import static com.epam.aidial.core.storage.http.HttpStatus.NOT_FOUND; import static com.epam.aidial.core.storage.http.HttpStatus.UNSUPPORTED_MEDIA_TYPE; @@ -576,6 +577,128 @@ public void testHandleRequestBody_OverrideModelName_Application() throws IOExcep assertEquals(tree.get("model").asText(), "overrideName"); } + @Test + public void testHandleRequestBody_SkillAutoShared_WhenReadable() { + when(context.getRequest()).thenReturn(request); + UpstreamRoute upstreamRoute = mock(UpstreamRoute.class, RETURNS_DEEP_STUBS); + when(upstreamRoute.next()).thenReturn(new Upstream("endpoint", null, null, null, null, 0, 0, null, null, null)); + when(context.getUpstreamRoute()).thenReturn(upstreamRoute); + HttpServerRequest request = mock(HttpServerRequest.class, RETURNS_DEEP_STUBS); + when(context.getRequest()).thenReturn(request); + when(request.path()).thenReturn("/openai/deployments/name/chat/completions"); + when(proxy.getClient()).thenReturn(mock(HttpClient.class, RETURNS_DEEP_STUBS)); + when(proxy.getApiKeyStore()).thenReturn(mock(ApiKeyStore.class)); + when(proxy.getClientOptions()).thenReturn(new HttpClientOptions()); + ApiKeyData proxyApiKeyData = new ApiKeyData(); + proxyApiKeyData.setInterceptorIndex(0); + when(context.getProxyApiKeyData()).thenReturn(proxyApiKeyData); + when(proxy.getEncryptionService().decrypt("bucket")).thenReturn("location/"); + when(proxy.getAccessService().hasReadAccess(any(), any())).thenReturn(true); + + Model model = new Model(); + model.setName("name"); + model.setEndpoint("http://host/model"); + when(context.getDeployment()).thenReturn(model); + String body = """ + { + "model": "name", + "messages": [ + { + "role": "user", + "content": "use the summarizer skill", + "custom_content": { + "skills": [ + {"url": "skills/bucket/summarizer"} + ] + } + } + ], + "stream": false + } + """; + Buffer requestBody = Buffer.buffer(body); + + controller.handleRequestBody(requestBody); + + assertNotNull(proxyApiKeyData.getAttachedSkills().get("skills/bucket/summarizer")); + } + + @Test + public void testHandleRequestBody_SkillAccessDenied() { + when(context.getRequest()).thenReturn(request); + HttpServerRequest request = mock(HttpServerRequest.class, RETURNS_DEEP_STUBS); + when(context.getRequest()).thenReturn(request); + when(request.path()).thenReturn("/openai/deployments/name/chat/completions"); + ApiKeyData proxyApiKeyData = new ApiKeyData(); + when(context.getProxyApiKeyData()).thenReturn(proxyApiKeyData); + when(proxy.getEncryptionService().decrypt("bucket")).thenReturn("location/"); + // proxy.getAccessService().hasReadAccess(...) defaults to false (deep stub) + + Model model = new Model(); + model.setName("name"); + model.setEndpoint("http://host/model"); + when(context.getDeployment()).thenReturn(model); + String body = """ + { + "model": "name", + "messages": [ + { + "role": "user", + "content": "use the summarizer skill", + "custom_content": { + "skills": [ + {"url": "skills/bucket/summarizer"} + ] + } + } + ], + "stream": false + } + """; + Buffer requestBody = Buffer.buffer(body); + + controller.handleRequestBody(requestBody); + + verify(context).respond(eq(FORBIDDEN), anyString()); + } + + @Test + public void testHandleRequestBody_SkillUrlIsNotSkillResource() { + when(context.getRequest()).thenReturn(request); + HttpServerRequest request = mock(HttpServerRequest.class, RETURNS_DEEP_STUBS); + when(context.getRequest()).thenReturn(request); + when(request.path()).thenReturn("/openai/deployments/name/chat/completions"); + ApiKeyData proxyApiKeyData = new ApiKeyData(); + when(context.getProxyApiKeyData()).thenReturn(proxyApiKeyData); + + Model model = new Model(); + model.setName("name"); + model.setEndpoint("http://host/model"); + when(context.getDeployment()).thenReturn(model); + String body = """ + { + "model": "name", + "messages": [ + { + "role": "user", + "content": "use the summarizer skill", + "custom_content": { + "skills": [ + {"url": "files/public/readme.md"} + ] + } + } + ], + "stream": false + } + """; + Buffer requestBody = Buffer.buffer(body); + + controller.handleRequestBody(requestBody); + + verify(context).respond(eq(BAD_REQUEST), eq("Url must reference a skill resource: files/public/readme.md")); + } + @Test public void testHandleRequestBody_UseUpstreamWithoutEndpoint() { when(context.getRequest()).thenReturn(request); diff --git a/server/src/test/java/com/epam/aidial/core/server/function/CollectRequestSkillsFnTest.java b/server/src/test/java/com/epam/aidial/core/server/function/CollectRequestSkillsFnTest.java new file mode 100644 index 000000000..611d24f61 --- /dev/null +++ b/server/src/test/java/com/epam/aidial/core/server/function/CollectRequestSkillsFnTest.java @@ -0,0 +1,116 @@ +package com.epam.aidial.core.server.function; + +import com.epam.aidial.core.server.Proxy; +import com.epam.aidial.core.server.ProxyContext; +import com.epam.aidial.core.server.data.ApiKeyData; +import com.epam.aidial.core.server.function.request.RequestObject; +import com.epam.aidial.core.server.security.AccessService; +import com.epam.aidial.core.server.security.EncryptionService; +import com.epam.aidial.core.storage.http.HttpException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class CollectRequestSkillsFnTest { + + @Mock + private Proxy proxy; + + @Mock + private ProxyContext context; + + @Mock + private AccessService accessService; + + @Mock + private EncryptionService encryptionService; + + @InjectMocks + private CollectRequestSkillsFn fn; + + private RequestObject request; + + @BeforeEach + void setUp() { + request = mock(RequestObject.class); + } + + @Test + void apply_appendsSkillToApiKeyData_whenSkillIsReadable() { + String skillUrl = "skills/bucket/my-skill"; + when(request.collectSkills()).thenReturn(Set.of(skillUrl)); + when(proxy.getEncryptionService()).thenReturn(encryptionService); + when(encryptionService.decrypt("bucket")).thenReturn("location/"); + when(proxy.getAccessService()).thenReturn(accessService); + when(accessService.hasReadAccess(any(), any())).thenReturn(true); + ApiKeyData apiKeyData = new ApiKeyData(); + when(context.getProxyApiKeyData()).thenReturn(apiKeyData); + + boolean result = fn.apply(request); + + assertFalse(result); + assertNotNull(apiKeyData.getAttachedSkills().get(skillUrl)); + assertEquals(1, apiKeyData.getAttachedSkills().size()); + } + + @Test + void apply_doesNotAppendSkill_whenSkillIsPublic() { + String skillUrl = "skills/public/my-skill"; + when(request.collectSkills()).thenReturn(Set.of(skillUrl)); + when(proxy.getEncryptionService()).thenReturn(encryptionService); + + boolean result = fn.apply(request); + + assertFalse(result); + } + + @Test + void apply_throws_whenAccessServiceHasNoReadAccess() { + String skillUrl = "skills/bucket/my-skill"; + when(request.collectSkills()).thenReturn(Set.of(skillUrl)); + when(proxy.getEncryptionService()).thenReturn(encryptionService); + when(encryptionService.decrypt("bucket")).thenReturn("location/"); + when(proxy.getAccessService()).thenReturn(accessService); + when(accessService.hasReadAccess(any(), any())).thenReturn(false); + + HttpException error = Assertions.assertThrows(HttpException.class, () -> fn.apply(request)); + + assertEquals(403, error.getStatus().getCode()); + } + + @Test + void apply_throws_whenUrlIsNotSkillResource() { + String fileUrl = "files/bucket/my-file"; + when(request.collectSkills()).thenReturn(Set.of(fileUrl)); + when(proxy.getEncryptionService()).thenReturn(encryptionService); + when(encryptionService.decrypt("bucket")).thenReturn("location/"); + + HttpException error = Assertions.assertThrows(HttpException.class, () -> fn.apply(request)); + + assertEquals(400, error.getStatus().getCode()); + } + + @Test + void apply_throws_whenUrlIsAbsolute() { + String url = "http://example.com/skill"; + when(request.collectSkills()).thenReturn(Set.of(url)); + + HttpException error = Assertions.assertThrows(HttpException.class, () -> fn.apply(request)); + + assertEquals(400, error.getStatus().getCode()); + } +} diff --git a/server/src/test/java/com/epam/aidial/core/server/function/request/ChatCompletionRequestTest.java b/server/src/test/java/com/epam/aidial/core/server/function/request/ChatCompletionRequestTest.java index 96f720957..b7d6c21ce 100644 --- a/server/src/test/java/com/epam/aidial/core/server/function/request/ChatCompletionRequestTest.java +++ b/server/src/test/java/com/epam/aidial/core/server/function/request/ChatCompletionRequestTest.java @@ -268,6 +268,105 @@ void testCollectAttachedFiles_EmbeddingRequest_invalid() throws IOException { assertEquals(Set.of(), actual); } + @Test + void testCollectSkills_ChatRequest() throws IOException { + String body = """ + { + "modelId": "model", + "messages": [ + { + "content": "test", + "role": "user", + "custom_content": { + } + }, + { + "content": "use the summarizer skill", + "role": "user", + "custom_content": { + "skills": [ + { + "url": "skills/7G9WZNcoY26Vy9D7bEgbv6zqbJGfyDp9KZyEbJR4XMZt/summarizer" + } + ] + } + }, + { + "content": "Sure, using the summarizer skill.", + "role": "assistant", + "custom_content": { + "skills": [ + { + "url": "skills/7G9WZNcoY26Vy9D7bEgbv6zqbJGfyDp9KZyEbJR4XMZt/translator" + } + ] + } + } + ], + "id": "id" + } + """; + ChatCompletionRequest request = request(body); + Set expected = Set.of( + "skills/7G9WZNcoY26Vy9D7bEgbv6zqbJGfyDp9KZyEbJR4XMZt/summarizer", + "skills/7G9WZNcoY26Vy9D7bEgbv6zqbJGfyDp9KZyEbJR4XMZt/translator"); + + Set actual = request.collectSkills(); + + assertEquals(expected, actual); + } + + @Test + void testCollectSkills_NoSkills() throws IOException { + String body = """ + { + "messages": [ + { + "content": "test", + "role": "user", + "custom_content": { + "attachments": [ + { + "type": "application/octet-stream", + "url": "files/7G9WZNcoY26Vy9D7bEgbv6zqbJGfyDp9KZyEbJR4XMZt/b1/Dockerfile" + } + ] + } + } + ] + } + """; + ChatCompletionRequest request = request(body); + + assertEquals(Set.of(), request.collectSkills()); + } + + @Test + void testCollectSkills_MissingUrl_Fail() throws IOException { + String body = """ + { + "messages": [ + { + "content": "test", + "role": "user", + "custom_content": { + "skills": [ + { + "title": "summarizer" + } + ] + } + } + ] + } + """; + ChatCompletionRequest request = request(body); + + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, request::collectSkills); + + assertEquals("Missing url in skill attachment.", error.getMessage()); + } + private static ChatCompletionRequest request(String body) throws JsonProcessingException { return new ChatCompletionRequest((ObjectNode) ProxyUtil.MAPPER.readTree(body)); } From fa7f6dbc7e233900a3a50c202f827e0abb9f953f Mon Sep 17 00:00:00 2001 From: Aliaksandr Stsiapanay Date: Thu, 10 Sep 2026 16:34:32 +0300 Subject: [PATCH 2/2] fix: source RequestSkill schema from openapi-generator fragments #1955 docs/open_api_core.yaml is a generated+merged artifact: schemas without a real Java DTO are sourced from hand-authored fragments under openapi-generator/src/main/resources/schemas/, and SpecMerger deletes any top-level schema in the manual doc that isn't backed by one. The RequestSkill schema added directly to docs/open_api_core.yaml was being dropped as orphaned, breaking `./gradlew replaceSpec -Plint`. Add the RequestSkill fragment and reference it from the two CustomContent fragments instead, then regenerate the doc through the real pipeline. --- docs/open_api_core.yaml | 4 ++-- ...ChatCompletionRequestAssistantMessageCustomContent.yaml | 5 +++++ .../ChatCompletionRequestUserMessageCustomContent.yaml | 5 +++++ .../src/main/resources/schemas/RequestSkill.yaml | 7 +++++++ 4 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 openapi-generator/src/main/resources/schemas/RequestSkill.yaml diff --git a/docs/open_api_core.yaml b/docs/open_api_core.yaml index 0c24dd71d..e50cfae5c 100644 --- a/docs/open_api_core.yaml +++ b/docs/open_api_core.yaml @@ -16972,13 +16972,13 @@ components: type: string description: "If `reference_type` is specified, the content of `reference_url` should follow the format described in the MIME standard for `reference_type`." RequestSkill: - type: object required: - url + type: object properties: url: type: string - description: The URL of the skill resource, e.g. `skills//`. + description: "The URL of the skill resource, e.g. `skills//`." ResourceAccessType: type: string enum: diff --git a/openapi-generator/src/main/resources/schemas/ChatCompletionRequestAssistantMessageCustomContent.yaml b/openapi-generator/src/main/resources/schemas/ChatCompletionRequestAssistantMessageCustomContent.yaml index f94a7d299..8bc2d4a86 100644 --- a/openapi-generator/src/main/resources/schemas/ChatCompletionRequestAssistantMessageCustomContent.yaml +++ b/openapi-generator/src/main/resources/schemas/ChatCompletionRequestAssistantMessageCustomContent.yaml @@ -15,3 +15,8 @@ properties: description: |- The JSON schema describing a form that the assistant prompts the user to fill in. Given this schema, the user is expected to provide a JSON value in the next message in the `custom_content.form_value` field. + skills: + type: array + items: + $ref: "#/components/schemas/RequestSkill" + description: List of skills referenced by the model as part of the response. diff --git a/openapi-generator/src/main/resources/schemas/ChatCompletionRequestUserMessageCustomContent.yaml b/openapi-generator/src/main/resources/schemas/ChatCompletionRequestUserMessageCustomContent.yaml index 3440d035b..1d6a8281f 100644 --- a/openapi-generator/src/main/resources/schemas/ChatCompletionRequestUserMessageCustomContent.yaml +++ b/openapi-generator/src/main/resources/schemas/ChatCompletionRequestUserMessageCustomContent.yaml @@ -10,3 +10,8 @@ properties: type: object description: |- The JSON value corresponding to the JSON schema sent by the assistant in the previous message in the `custom_content.form_schema` field. + skills: + type: array + items: + $ref: "#/components/schemas/RequestSkill" + description: List of skills used to supply an additional input for the model. diff --git a/openapi-generator/src/main/resources/schemas/RequestSkill.yaml b/openapi-generator/src/main/resources/schemas/RequestSkill.yaml new file mode 100644 index 000000000..0c673a683 --- /dev/null +++ b/openapi-generator/src/main/resources/schemas/RequestSkill.yaml @@ -0,0 +1,7 @@ +type: object +properties: + url: + type: string + description: The URL of the skill resource, e.g. `skills//`. +required: + - url