Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ public ChatResponse internalCall(Prompt prompt, ChatResponse previousChatRespons
ctx -> this.anthropicApi.chatCompletionEntity(request, this.getAdditionalHttpHeaders(prompt)));

AnthropicApi.ChatCompletionResponse completionResponse = completionEntity.getBody();

AnthropicApi.Usage usage = completionResponse.usage();

Usage currentChatResponseUsage = usage != null ? this.getDefaultUsage(completionResponse.usage())
Expand Down Expand Up @@ -381,7 +382,8 @@ private ChatResponse toChatResponse(ChatCompletionResponse chatCompletion, Usage
.usage(usage)
.keyValue("stop-reason", chatCompletion.stopReason())
.keyValue("stop-sequence", chatCompletion.stopSequence())
.keyValue("type", chatCompletion.type());
.keyValue("type", chatCompletion.type())
.keyValue("anthropic-response", chatCompletion);

// Add citation metadata if citations were found
if (citationContext.hasCitations()) {
Expand Down Expand Up @@ -583,6 +585,14 @@ Prompt buildRequestPrompt(Prompt prompt) {
else if (this.defaultOptions.getCitationDocuments() != null) {
requestOptions.setCitationDocuments(this.defaultOptions.getCitationDocuments());
}

// Merge skillContainer that is Json-ignored
if (runtimeOptions.getSkillContainer() != null) {
requestOptions.setSkillContainer(runtimeOptions.getSkillContainer());
}
else if (this.defaultOptions.getSkillContainer() != null) {
requestOptions.setSkillContainer(this.defaultOptions.getSkillContainer());
}
}
else {
requestOptions.setHttpHeaders(this.defaultOptions.getHttpHeaders());
Expand All @@ -591,6 +601,7 @@ else if (this.defaultOptions.getCitationDocuments() != null) {
requestOptions.setToolCallbacks(this.defaultOptions.getToolCallbacks());
requestOptions.setToolContext(this.defaultOptions.getToolContext());
requestOptions.setCitationDocuments(this.defaultOptions.getCitationDocuments());
requestOptions.setSkillContainer(this.defaultOptions.getSkillContainer());
}

ToolCallingChatOptions.validateToolCallbacks(requestOptions.getToolCallbacks());
Expand Down Expand Up @@ -628,7 +639,16 @@ ChatCompletionRequest createRequest(Prompt prompt, boolean stream) {
ChatCompletionRequest request = new ChatCompletionRequest(this.defaultOptions.getModel(), userMessages,
systemContent, this.defaultOptions.getMaxTokens(), this.defaultOptions.getTemperature(), stream);

request = ModelOptionsUtils.merge(requestOptions, request, ChatCompletionRequest.class);
// Save toolChoice for later application (after code_execution tool is added)
AnthropicApi.ToolChoice savedToolChoice = requestOptions != null ? requestOptions.getToolChoice() : null;
AnthropicChatOptions mergeOptions = requestOptions;
if (savedToolChoice != null && requestOptions != null) {
// Create a copy without toolChoice to avoid premature merge
mergeOptions = requestOptions.copy();
mergeOptions.setToolChoice(null);
}

request = ModelOptionsUtils.merge(mergeOptions, request, ChatCompletionRequest.class);

// Add the tool definitions with potential caching
List<ToolDefinition> toolDefinitions = this.toolCallingManager.resolveToolDefinitions(requestOptions);
Expand All @@ -642,21 +662,112 @@ ChatCompletionRequest createRequest(Prompt prompt, boolean stream) {
request = ChatCompletionRequest.from(request).tools(tools).build();
}

// Add beta header for 1-hour TTL if needed
if (cacheOptions.getMessageTypeTtl().containsValue(AnthropicCacheTtl.ONE_HOUR)) {
// Add Skills container from options if present
AnthropicApi.SkillContainer skillContainer = null;
if (requestOptions != null && requestOptions.getSkillContainer() != null) {
skillContainer = requestOptions.getSkillContainer();
}
else if (this.defaultOptions.getSkillContainer() != null) {
skillContainer = this.defaultOptions.getSkillContainer();
}

if (skillContainer != null) {
request = ChatCompletionRequest.from(request).container(skillContainer).build();

// Skills require the code_execution tool to be enabled
// Add it if not already present
List<AnthropicApi.Tool> existingTools = request.tools() != null ? new ArrayList<>(request.tools())
: new ArrayList<>();
boolean hasCodeExecution = existingTools.stream().anyMatch(tool -> "code_execution".equals(tool.name()));

if (!hasCodeExecution) {
existingTools
.add(new AnthropicApi.Tool(AnthropicApi.CODE_EXECUTION_TOOL_TYPE, "code_execution", null, null));
request = ChatCompletionRequest.from(request).tools(existingTools).build();
}

// Apply saved toolChoice now that code_execution tool has been added
if (savedToolChoice != null) {
request = ChatCompletionRequest.from(request).toolChoice(savedToolChoice).build();
}
}
else if (savedToolChoice != null) {
// No Skills but toolChoice was set - apply it now
request = ChatCompletionRequest.from(request).toolChoice(savedToolChoice).build();
}

// Add beta headers if needed
if (requestOptions != null) {
Map<String, String> headers = new HashMap<>(requestOptions.getHttpHeaders());
headers.put("anthropic-beta", AnthropicApi.BETA_EXTENDED_CACHE_TTL);
requestOptions.setHttpHeaders(headers);
boolean needsUpdate = false;

// Add Skills beta headers if Skills are present
// Skills require three beta headers: skills, code-execution, and files-api
if (skillContainer != null) {
String existingBeta = headers.get("anthropic-beta");
String requiredBetas = AnthropicApi.BETA_SKILLS + "," + AnthropicApi.BETA_CODE_EXECUTION + ","
+ AnthropicApi.BETA_FILES_API;

if (existingBeta != null) {
// Add missing beta headers
if (!existingBeta.contains(AnthropicApi.BETA_SKILLS)) {
existingBeta = existingBeta + "," + AnthropicApi.BETA_SKILLS;
}
if (!existingBeta.contains(AnthropicApi.BETA_CODE_EXECUTION)) {
existingBeta = existingBeta + "," + AnthropicApi.BETA_CODE_EXECUTION;
}
if (!existingBeta.contains(AnthropicApi.BETA_FILES_API)) {
existingBeta = existingBeta + "," + AnthropicApi.BETA_FILES_API;
}
headers.put("anthropic-beta", existingBeta);
}
else {
headers.put("anthropic-beta", requiredBetas);
}
needsUpdate = true;
}

// Add extended cache TTL beta header if needed
if (cacheOptions.getMessageTypeTtl().containsValue(AnthropicCacheTtl.ONE_HOUR)) {
String existingBeta = headers.get("anthropic-beta");
if (existingBeta != null && !existingBeta.contains(AnthropicApi.BETA_EXTENDED_CACHE_TTL)) {
headers.put("anthropic-beta", existingBeta + "," + AnthropicApi.BETA_EXTENDED_CACHE_TTL);
}
else if (existingBeta == null) {
headers.put("anthropic-beta", AnthropicApi.BETA_EXTENDED_CACHE_TTL);
}
needsUpdate = true;
}

if (needsUpdate) {
requestOptions.setHttpHeaders(headers);
}
}

return request;
}

/**
* Helper method to serialize content from ContentBlock. The content field can be
* either a String or a complex object (for Skills responses).
* @param content The content to serialize
* @return String representation of the content, or null if content is null
*/
private static String serializeContent(Object content) {
if (content == null) {
return null;
}
if (content instanceof String s) {
return s;
}
return JsonParser.toJson(content);
}

private static ContentBlock cacheAwareContentBlock(ContentBlock contentBlock, MessageType messageType,
CacheEligibilityResolver cacheEligibilityResolver) {
String basisForLength = switch (contentBlock.type()) {
case TEXT, TEXT_DELTA -> contentBlock.text();
case TOOL_RESULT -> contentBlock.content();
case TOOL_RESULT -> serializeContent(contentBlock.content());
case TOOL_USE -> JsonParser.toJson(contentBlock.input());
case THINKING, THINKING_DELTA -> contentBlock.thinking();
case REDACTED_THINKING -> contentBlock.data();
Expand Down Expand Up @@ -845,7 +956,8 @@ private List<AnthropicApi.Tool> addCacheToLastTool(List<AnthropicApi.Tool> tools
AnthropicApi.Tool tool = tools.get(i);
if (i == tools.size() - 1) {
// Add cache control to last tool
tool = new AnthropicApi.Tool(tool.name(), tool.description(), tool.inputSchema(), cacheControl);
tool = new AnthropicApi.Tool(tool.type(), tool.name(), tool.description(), tool.inputSchema(),
cacheControl);
cacheEligibilityResolver.useCacheBlock();
}
modifiedTools.add(tool);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,24 @@ public AnthropicCacheOptions getCacheOptions() {
public void setCacheOptions(AnthropicCacheOptions cacheOptions) {
this.cacheOptions = cacheOptions;
}

/**
* Container for Claude Skills to make available in this request.
* Skills are collections of instructions, scripts, and resources that
* extend Claude's capabilities for specific domains.
* Maximum of 8 skills per request.
*/
@JsonIgnore
private AnthropicApi.SkillContainer skillContainer;

public AnthropicApi.SkillContainer getSkillContainer() {
return this.skillContainer;
}

public void setSkillContainer(AnthropicApi.SkillContainer skillContainer) {
this.skillContainer = skillContainer;
}

/**
* Collection of {@link ToolCallback}s to be used for tool calling in the chat
* completion requests.
Expand Down Expand Up @@ -141,6 +159,7 @@ public static AnthropicChatOptions fromOptions(AnthropicChatOptions fromOptions)
.cacheOptions(fromOptions.getCacheOptions())
.citationDocuments(fromOptions.getCitationDocuments() != null
? new ArrayList<>(fromOptions.getCitationDocuments()) : null)
.skillContainer(fromOptions.getSkillContainer())
.build();
}

Expand Down Expand Up @@ -351,15 +370,16 @@ public boolean equals(Object o) {
&& Objects.equals(this.toolContext, that.toolContext)
&& Objects.equals(this.httpHeaders, that.httpHeaders)
&& Objects.equals(this.cacheOptions, that.cacheOptions)
&& Objects.equals(this.citationDocuments, that.citationDocuments);
&& Objects.equals(this.citationDocuments, that.citationDocuments)
&& Objects.equals(this.skillContainer, that.skillContainer);
}

@Override
public int hashCode() {
return Objects.hash(this.model, this.maxTokens, this.metadata, this.stopSequences, this.temperature, this.topP,
this.topK, this.toolChoice, this.thinking, this.toolCallbacks, this.toolNames,
this.internalToolExecutionEnabled, this.toolContext, this.httpHeaders, this.cacheOptions,
this.citationDocuments);
this.citationDocuments, this.skillContainer);
}

public static final class Builder {
Expand Down Expand Up @@ -501,6 +521,89 @@ public Builder addCitationDocument(CitationDocument document) {
return this;
}

/**
* Set the Skills container for this request.
* @param skillContainer Container with skills to make available
* @return Builder for method chaining
*/
public Builder skillContainer(AnthropicApi.SkillContainer skillContainer) {
this.options.setSkillContainer(skillContainer);
return this;
}

/**
* Add a single skill to the request. Creates a SkillContainer if one doesn't
* exist.
* @param skill Skill to add
* @return Builder for method chaining
*/
public Builder skill(AnthropicApi.Skill skill) {
Assert.notNull(skill, "Skill cannot be null");
if (this.options.skillContainer == null) {
this.options.skillContainer = AnthropicApi.SkillContainer.builder().skill(skill).build();
}
else {
// Rebuild container with additional skill
List<AnthropicApi.Skill> existingSkills = new ArrayList<>(this.options.skillContainer.skills());
existingSkills.add(skill);
this.options.skillContainer = new AnthropicApi.SkillContainer(existingSkills);
}
return this;
}

/**
* Add an Anthropic pre-built skill (xlsx, pptx, docx, pdf).
*
* <p>
* Example: <pre>{@code
* AnthropicChatOptions options = AnthropicChatOptions.builder()
* .model("claude-sonnet-4-5")
* .anthropicSkill(AnthropicSkill.XLSX)
* .anthropicSkill(AnthropicSkill.PPTX)
* .build();
* }</pre>
* @param anthropicSkill Pre-built Anthropic skill to add
* @return Builder for method chaining
*/
public Builder anthropicSkill(AnthropicApi.AnthropicSkill anthropicSkill) {
Assert.notNull(anthropicSkill, "AnthropicSkill cannot be null");
return skill(anthropicSkill.toSkill());
}

/**
* Add an Anthropic pre-built skill with specific version.
* @param anthropicSkill Pre-built Anthropic skill to add
* @param version Version of the skill (e.g., "latest", "20251013")
* @return Builder for method chaining
*/
public Builder anthropicSkill(AnthropicApi.AnthropicSkill anthropicSkill, String version) {
Assert.notNull(anthropicSkill, "AnthropicSkill cannot be null");
Assert.hasText(version, "Version cannot be empty");
return skill(anthropicSkill.toSkill(version));
}

/**
* Add a custom skill by ID.
* @param skillId Custom skill ID
* @return Builder for method chaining
*/
public Builder customSkill(String skillId) {
Assert.hasText(skillId, "Skill ID cannot be empty");
return skill(new AnthropicApi.Skill(AnthropicApi.SkillType.CUSTOM, skillId));
}

/**
* Add a custom skill with specific version.
* @param skillId Custom skill ID
* @param version Version of the skill
* @return Builder for method chaining
*/
public Builder customSkill(String skillId, String version) {
Assert.hasText(skillId, "Skill ID cannot be empty");
Assert.hasText(version, "Version cannot be empty");
return skill(new AnthropicApi.Skill(AnthropicApi.SkillType.CUSTOM, skillId, version));
}

public AnthropicChatOptions build() {
this.options.validateCitationConsistency();
return this.options;
Expand Down
Loading