diff --git a/README.md b/README.md index c2db0a2..98d870c 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/fumito-ito/AnthropicSwiftSDK.git", .upToNextMajor(from: "0.8.0")) + .package(url: "https://github.com/fumito-ito/AnthropicSwiftSDK.git", .upToNextMajor(from: "0.11.0")) ] ) ``` @@ -178,6 +178,30 @@ let response = try await anthropic.messages.createMessage( ) ``` +### [Token Counting](https://docs.anthropic.com/en/docs/build-with-claude/token-counting) + +Token counting enables you to determine the number of tokens in a message before sending it to Claude, helping you make informed decisions about your prompts and usage. With token counting, you can + +- Proactively manage rate limits and costs +- Make smart model routing decisions +- Optimize prompts to be a specific length + +```swift +let anthropic = Anthropic(apiKey: "YOUR_OWN_API_KEY") + +let message = Message(role: .user, content: [.text("Find flights from San Francisco to a place with warmer weather.")]) +let response = try await anthropic.countTokens.countTokens( + [message], + maxTokens: 1024, + tools: [ + .computer(.init(name: "my_computer", displayWidthPx: 1024, displayHeightPx: 768, displayNumber: 1), + .bash(.init(name: "bash")) + ] +) +``` + +The token counting endpoint accepts the same structured list of inputs for creating a message, including support for system prompts, tools, images, and PDFs. The response contains the total number of input tokens. + ## Extensions By introducing an extension Swift package, it is possible to access the Anthropic Claude API through AWS Bedrock and Vertex AI. The supported services are as follows: diff --git a/Sources/AnthropicSwiftSDK/Anthropic.swift b/Sources/AnthropicSwiftSDK/Anthropic.swift index 6d35b5c..24ab227 100644 --- a/Sources/AnthropicSwiftSDK/Anthropic.swift +++ b/Sources/AnthropicSwiftSDK/Anthropic.swift @@ -15,10 +15,14 @@ public final class Anthropic { /// MessageBatches API Interface public let messageBatches: MessageBatches + /// Token Counting API Interface + public let countTokens: CountTokens + /// Construction of SDK /// - Parameter apiKey: API key to access Anthropic API. public init(apiKey: String) { self.messages = Messages(apiKey: apiKey, session: .shared) self.messageBatches = MessageBatches(apiKey: apiKey, session: .shared) + self.countTokens = CountTokens(apiKey: apiKey, session: .shared) } } diff --git a/Sources/AnthropicSwiftSDK/CountTokens.swift b/Sources/AnthropicSwiftSDK/CountTokens.swift new file mode 100644 index 0000000..c2eb5cc --- /dev/null +++ b/Sources/AnthropicSwiftSDK/CountTokens.swift @@ -0,0 +1,105 @@ +// +// CountTokens.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/11/13. +// +import Foundation + +public struct CountTokens { + /// The API key used for authentication with the Anthropic API. + private let apiKey: String + /// The URL session used for network requests. + private let session: URLSession + + /// Initializes a new instance of `MessageBatches`. + /// + /// - Parameters: + /// - apiKey: The API key for authentication. + /// - session: The URL session for network requests. + init(apiKey: String, session: URLSession) { + self.apiKey = apiKey + self.session = session + } + + public func countTokens( + _ messages: [Message], + model: Model = .claude_3_Opus, + system: [SystemPrompt] = [], + maxTokens: Int, + metaData: MetaData? = nil, + stopSequence: [String]? = nil, + temperature: Double? = nil, + topP: Double? = nil, + topK: Int? = nil, + tools: [Tool]? = nil, + toolChoice: ToolChoice = .auto + ) async throws -> CountTokenResponse { + try await countTokens( + messages, + model: model, + system: system, + maxTokens: maxTokens, + metaData: metaData, + stopSequence: stopSequence, + temperature: temperature, + topP: topP, + topK: topK, + tools: tools, + toolChoice: toolChoice, + anthropicHeaderProvider: DefaultAnthropicHeaderProvider(), + authenticationHeaderProvider: APIKeyAuthenticationHeaderProvider(apiKey: apiKey) + ) + } + + public func countTokens( + _ messages: [Message], + model: Model = .claude_3_Opus, + system: [SystemPrompt] = [], + maxTokens: Int, + metaData: MetaData? = nil, + stopSequence: [String]? = nil, + temperature: Double? = nil, + topP: Double? = nil, + topK: Int? = nil, + tools: [Tool]? = nil, + toolChoice: ToolChoice = .auto, + anthropicHeaderProvider: AnthropicHeaderProvider, + authenticationHeaderProvider: AuthenticationHeaderProvider + ) async throws -> CountTokenResponse { + let client = APIClient( + session: session, + anthropicHeaderProvider: anthropicHeaderProvider, + authenticationHeaderProvider: authenticationHeaderProvider + ) + + let request = CountTokenRequest( + body: .init( + model: model, + messages: messages, + system: system, + maxTokens: maxTokens, + metaData: metaData, + stopSequences: stopSequence, + stream: false, + temperature: temperature, + topP: topP, + topK: topK, + tools: tools, + toolChoice: toolChoice + ) + ) + + let (data, response) = try await client.send(request: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw ClientError.cannotHandleURLResponse(response) + } + + guard httpResponse.statusCode == 200 else { + throw AnthropicAPIError(fromHttpStatusCode: httpResponse.statusCode) + } + + return try anthropicJSONDecoder.decode(CountTokenResponse.self, from: data) + } +} diff --git a/Sources/AnthropicSwiftSDK/Network/HeaderProvider/AnthropicHeaderProvider.swift b/Sources/AnthropicSwiftSDK/Network/HeaderProvider/AnthropicHeaderProvider.swift index 67d3b56..dcbf0eb 100644 --- a/Sources/AnthropicSwiftSDK/Network/HeaderProvider/AnthropicHeaderProvider.swift +++ b/Sources/AnthropicSwiftSDK/Network/HeaderProvider/AnthropicHeaderProvider.swift @@ -62,4 +62,8 @@ public enum BetaFeatures: String, CaseIterable { /// /// https://docs.anthropic.com/en/docs/build-with-claude/pdf-support case pdfSupport = "pdfs-2024-09-25" + /// Token Counting (beta) + /// + /// https://docs.anthropic.com/en/docs/build-with-claude/token-counting + case tokenCounting = "token-counting-2024-11-01" } diff --git a/Sources/AnthropicSwiftSDK/Network/Request/CountTokenRequest.swift b/Sources/AnthropicSwiftSDK/Network/Request/CountTokenRequest.swift new file mode 100644 index 0000000..6859ddb --- /dev/null +++ b/Sources/AnthropicSwiftSDK/Network/Request/CountTokenRequest.swift @@ -0,0 +1,93 @@ +// +// CountTokenRequest.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/11/13. +// + +import Foundation + +struct CountTokenRequest: Request { + typealias Body = CountTokenRequestBody + + let method: HttpMethod = .post + let path: String = RequestType.countTokens.basePath + let queries: [String: CustomStringConvertible]? = nil + let body: Body? +} + +// MARK: Request Body + +/// Request object for Count Token API +/// +/// a structured list of input messages with text and/or image content, and the model will generate the next message in the conversation. +struct CountTokenRequestBody: Encodable { + /// The model that will complete your prompt. + let model: Model + /// Input messages. + let messages: [Message] + /// System prompt. + /// + /// A system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or role. + let system: [SystemPrompt] + /// The maximum number of tokens to generate before stopping. + /// + /// Note that our models may stop before reaching this maximum. This parameter only specifies the absolute maximum number of tokens to generate. + /// Different models have different maximum values for this parameter. + let maxTokens: Int + /// An object describing metadata about the request. + let metaData: MetaData? + /// Custom text sequences that will cause the model to stop generating. + let stopSequences: [String]? + /// Whether to incrementally stream the response using server-sent events. + /// + /// see [streaming](https://docs.anthropic.com/claude/reference/messages-streaming) for more detail. + let stream: Bool + /// Amount of randomness injected into the response. + /// + /// Defaults to 1.0. Ranges from 0.0 to 1.0. Use temperature closer to 0.0 for analytical / multiple choice, and closer to 1.0 for creative and generative tasks. + /// Note that even with temperature of 0.0, the results will not be fully deterministic. + let temperature: Double? + /// Use nucleus sampling. + /// + /// In nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and cut it off once it reaches a particular probability specified by top_p. You should either alter temperature or top_p, but not both. + /// Recommended for advanced use cases only. You usually only need to use temperature. + let topP: Double? + /// Only sample from the top K options for each subsequent token. + /// + /// Used to remove "long tail" low probability responses. + /// Recommended for advanced use cases only. You usually only need to use temperature. + let topK: Int? + /// Definition of tools with names, descriptions, and input schemas in your API request. + let tools: [Tool]? + /// Definition whether or not to force Claude to use the tool. ToolChoice should be set if tools are specified. + let toolChoice: ToolChoice? + + init( + model: Model = .claude_3_Opus, + messages: [Message], + system: [SystemPrompt] = [], + maxTokens: Int, + metaData: MetaData? = nil, + stopSequences: [String]? = nil, + stream: Bool = false, + temperature: Double? = nil, + topP: Double? = nil, + topK: Int? = nil, + tools: [Tool]? = nil, + toolChoice: ToolChoice = .auto + ) { + self.model = model + self.messages = messages + self.system = system + self.maxTokens = maxTokens + self.metaData = metaData + self.stopSequences = stopSequences + self.stream = stream + self.temperature = temperature + self.topP = topP + self.topK = topK + self.tools = tools + self.toolChoice = tools == nil ? nil : toolChoice // ToolChoice should be set if tools are specified. + } +} diff --git a/Sources/AnthropicSwiftSDK/Network/Request/Request.swift b/Sources/AnthropicSwiftSDK/Network/Request/Request.swift index 2b4f831..80e7b8d 100644 --- a/Sources/AnthropicSwiftSDK/Network/Request/Request.swift +++ b/Sources/AnthropicSwiftSDK/Network/Request/Request.swift @@ -15,6 +15,7 @@ public enum HttpMethod: String { enum RequestType { case messages case batches + case countTokens var basePath: String { switch self { @@ -22,6 +23,8 @@ enum RequestType { return "/v1/messages" case .batches: return "/v1/messages/batches" + case .countTokens: + return "v1/messages/count_tokens" } } } diff --git a/Sources/AnthropicSwiftSDK/Network/Response/CountTokenResponse.swift b/Sources/AnthropicSwiftSDK/Network/Response/CountTokenResponse.swift new file mode 100644 index 0000000..4215a17 --- /dev/null +++ b/Sources/AnthropicSwiftSDK/Network/Response/CountTokenResponse.swift @@ -0,0 +1,12 @@ +// +// CountTokenResponse.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/11/13. +// + +/// Billing and rate-limit usage. +public struct CountTokenResponse: Decodable { + /// The number of input tokens which were used. + public let inputTokens: Int? +} diff --git a/Tests/AnthropicSwiftSDKTests/Network/AnthropicAPIClientTests.swift b/Tests/AnthropicSwiftSDKTests/Network/AnthropicAPIClientTests.swift index c61889b..3d73270 100644 --- a/Tests/AnthropicSwiftSDKTests/Network/AnthropicAPIClientTests.swift +++ b/Tests/AnthropicSwiftSDKTests/Network/AnthropicAPIClientTests.swift @@ -69,7 +69,7 @@ final class AnthropicAPIClientTests: XCTestCase { XCTAssertEqual(headers!["x-api-key"], "test-api-key") XCTAssertEqual(headers!["anthropic-version"], "2023-06-01") XCTAssertEqual(headers!["Content-Type"], "application/json") - XCTAssertEqual(headers!["anthropic-beta"], "message-batches-2024-09-24,computer-use-2024-10-22,prompt-caching-2024-07-31,pdfs-2024-09-25") + XCTAssertEqual(headers!["anthropic-beta"], "message-batches-2024-09-24,computer-use-2024-10-22,prompt-caching-2024-07-31,pdfs-2024-09-25,token-counting-2024-11-01") expectation.fulfill() }, nil) @@ -90,7 +90,7 @@ final class AnthropicAPIClientTests: XCTestCase { XCTAssertEqual(headers!["x-api-key"], "test-api-key") XCTAssertEqual(headers!["anthropic-version"], "2023-06-01") XCTAssertEqual(headers!["Content-Type"], "application/json") - XCTAssertEqual(headers!["anthropic-beta"], "message-batches-2024-09-24,computer-use-2024-10-22,prompt-caching-2024-07-31,pdfs-2024-09-25") + XCTAssertEqual(headers!["anthropic-beta"], "message-batches-2024-09-24,computer-use-2024-10-22,prompt-caching-2024-07-31,pdfs-2024-09-25,token-counting-2024-11-01") expectation.fulfill() }, nil) diff --git a/Tests/AnthropicSwiftSDKTests/Network/HeaderProvider/DefaultAnthropicHeaderProviderTests.swift b/Tests/AnthropicSwiftSDKTests/Network/HeaderProvider/DefaultAnthropicHeaderProviderTests.swift index d45268e..0c6dba6 100644 --- a/Tests/AnthropicSwiftSDKTests/Network/HeaderProvider/DefaultAnthropicHeaderProviderTests.swift +++ b/Tests/AnthropicSwiftSDKTests/Network/HeaderProvider/DefaultAnthropicHeaderProviderTests.swift @@ -29,7 +29,7 @@ final class DefaultAnthropicHeaderProviderTests: XCTestCase { let provider = DefaultAnthropicHeaderProvider(useBeta: true) let headers = provider.getAnthropicAPIHeaders() - XCTAssertEqual(headers["anthropic-beta"], "message-batches-2024-09-24,computer-use-2024-10-22,prompt-caching-2024-07-31,pdfs-2024-09-25") + XCTAssertEqual(headers["anthropic-beta"], "message-batches-2024-09-24,computer-use-2024-10-22,prompt-caching-2024-07-31,pdfs-2024-09-25,token-counting-2024-11-01") } func testBetaHeaderShouldNotBeProvidedIfUseBeta() { diff --git a/Tests/AnthropicSwiftSDKTests/Network/Request/CountTokenRequestTests.swift b/Tests/AnthropicSwiftSDKTests/Network/Request/CountTokenRequestTests.swift new file mode 100644 index 0000000..b9abfa1 --- /dev/null +++ b/Tests/AnthropicSwiftSDKTests/Network/Request/CountTokenRequestTests.swift @@ -0,0 +1,72 @@ +// +// CountTokenRequestTests.swift +// AnthropicSwiftSDK +// +// Created by 伊藤史 on 2024/11/13. +// + +import XCTest +@testable import AnthropicSwiftSDK + +final class CountTokenRequestTests: XCTestCase { + func testEncoding() throws { + // Prepare test data + let message = Message(role: .user, content: [.text("Hello")]) + let systemPrompt = SystemPrompt.text("You are a helpful assistant", nil) + + let sut = CountTokenRequestBody( + model: .claude_3_Opus, + messages: [message], + system: [systemPrompt], + maxTokens: 1000, + metaData: .init(userId: "test-user"), + stopSequences: ["STOP"], + stream: true, + temperature: 0.7, + topP: 0.9, + topK: 10, + tools: nil, + toolChoice: .auto + ) + + // Encode to JSON + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + let data = try encoder.encode(sut) + let json = try JSONSerialization.jsonObject(with: data) as! [String: Any] + + // Verify basic properties + XCTAssertEqual(json["model"] as? String, "claude-3-opus-20240229") + XCTAssertEqual((json["messages"] as? [[String: Any]])?.count, 1) + XCTAssertEqual((json["system"] as? [[String: Any]])?.count, 1) + XCTAssertEqual(json["max_tokens"] as? Int, 1000) + XCTAssertNotNil(json["meta_data"]) + XCTAssertEqual((json["stop_sequences"] as? [String]), ["STOP"]) + XCTAssertEqual(json["stream"] as? Bool, true) + XCTAssertEqual(json["temperature"] as? Double, 0.7) + XCTAssertEqual(json["top_p"] as? Double, 0.9) + XCTAssertEqual(json["top_k"] as? Int, 10) + XCTAssertNil(json["tools"]) // Verify tools is nil + XCTAssertNil(json["tool_choice"]) + } + + func testEncodingWithMinimalParameters() throws { + // Test with only required parameters + let message = Message(role: .user, content: [.text("Hello")]) + let sut = CountTokenRequestBody( + messages: [message], + maxTokens: 1000 + ) + + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + let data = try encoder.encode(sut) + let json = try JSONSerialization.jsonObject(with: data) as! [String: Any] + + // Verify minimal configuration + XCTAssertEqual(json["model"] as? String, "claude-3-opus-20240229") + XCTAssertEqual((json["messages"] as? [[String: Any]])?.count, 1) + XCTAssertEqual(json["max_tokens"] as? Int, 1000) + XCTAssertNil(json["tool_choice"]) // Verify tool_choice is nil when tools are not specified + } +}