├── Examples └── SwiftAnthropicExample │ ├── SwiftAnthropicExample │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── SwiftAnthropicExampleApp.swift │ ├── ServiceSelectionView.swift │ ├── OptionsListView.swift │ ├── ApiKeyIntroView.swift │ ├── PhotoPicker.swift │ ├── AIProxyIntroView.swift │ ├── FunctionCalling │ │ ├── MessageFunctionCallingObservable.swift │ │ └── MessageFunctionCallingDemoView.swift │ ├── Thinking │ │ ├── ThinkingModeMessageDemoObservable.swift │ │ └── ThinkingModeMessageDemoView.swift │ ├── Skills │ │ ├── SkillsDemoObservable.swift │ │ └── SkillsDemoView.swift │ └── Messages │ │ └── MessageDemoObservable.swift │ ├── SwiftAnthropicExample.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved │ ├── SwiftAnthropicExampleUITests │ ├── SwiftAnthropicExampleUITestsLaunchTests.swift │ └── SwiftAnthropicExampleUITests.swift │ └── SwiftAnthropicExampleTests │ └── SwiftAnthropicExampleTests.swift ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Sources └── Anthropic │ ├── Public │ ├── ResponseModels │ │ ├── TextCompletion │ │ │ ├── TextCompletionStreamResponse.swift │ │ │ └── TextCompletionResponse.swift │ │ ├── Message │ │ │ ├── MessageInputTokens.swift │ │ │ ├── MessageResponse+DynamicContent.swift │ │ │ ├── MessageStreamResponse.swift │ │ │ └── MessageResponse.swift │ │ ├── ErrorResponse.swift │ │ └── Skill │ │ │ └── SkillResponse.swift │ ├── Parameters │ │ ├── Message │ │ │ ├── MessageTokenCountParameter.swift │ │ │ ├── MessageParameter+Web.swift │ │ │ └── JSONSchema.swift │ │ ├── Skill │ │ │ └── SkillParameter.swift │ │ └── TextCompletion │ │ │ └── TextCompletionParameter.swift │ ├── Model.swift │ └── StreamHandler.swift │ ├── Private │ ├── Network │ │ ├── AnthropicAPI.swift │ │ └── Endpoint.swift │ └── Networking │ │ ├── HTTPClient.swift │ │ ├── URLSessionHTTPClientAdapter.swift │ │ └── AsyncHTTPClientAdapter.swift │ ├── Service │ ├── AnthropicServiceFactory.swift │ ├── DefaultAnthropicService.swift │ └── AnthropicService.swift │ └── AIProxy │ ├── AIProxyCertificatePinning.swift │ ├── Endpoint+AIProxy.swift │ └── AIProxyService.swift ├── Tests └── SwiftAnthropicTests │ └── SwiftAnthropicTests.swift ├── .github └── workflows │ └── ci.yml ├── Package.swift └── Package.resolved /Examples/SwiftAnthropicExample/SwiftAnthropicExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExample/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExample/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExample/SwiftAnthropicExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftAnthropicExampleApp.swift 3 | // SwiftAnthropicExample 4 | // 5 | // Created by James Rochabrun on 2/24/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct SwiftAnthropicExampleApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ServiceSelectionView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Anthropic/Public/ResponseModels/TextCompletion/TextCompletionStreamResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextCompletionStreamResponse.swift 3 | // 4 | // 5 | // Created by James Rochabrun on 1/28/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// [Text Completion Response](https://docs.anthropic.com/claude/reference/streaming) 11 | public struct TextCompletionStreamResponse: Decodable { 12 | 13 | public let type: String 14 | 15 | public let completion: String? 16 | 17 | public let stopReason: String? 18 | 19 | public let model: String? 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Tests/SwiftAnthropicTests/SwiftAnthropicTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftAnthropic 3 | 4 | final class SwiftAnthropicTests: XCTestCase { 5 | func testEndpointConstruction() throws { 6 | let endpoint = AnthropicAPI( 7 | base: "https://api.example.org/my/path", 8 | apiPath: .messages 9 | ) 10 | let comp = endpoint.urlComponents( 11 | queryItems: [URLQueryItem(name: "query", value: "value")] 12 | ) 13 | XCTAssertEqual( 14 | "https://api.example.org/my/path/v1/messages?query=value", 15 | comp.url!.absoluteString 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build_and_test_macos: 11 | runs-on: macos-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Get swift version 15 | run: swift --version 16 | - name: Build 17 | run: swift build -q 18 | - name: Run tests 19 | run: swift test -q 20 | 21 | build_and_test_linux: 22 | runs-on: ubuntu-latest 23 | container: 24 | image: swift:6.0.1-jammy 25 | steps: 26 | - name: Install dependencies 27 | run: | 28 | apt-get update 29 | apt-get install -y curl git 30 | - uses: actions/checkout@v4 31 | - name: Get swift version 32 | run: swift --version 33 | - name: Build 34 | run: swift build -q 35 | - name: Run tests 36 | run: swift test -q -------------------------------------------------------------------------------- /Sources/Anthropic/Public/ResponseModels/Message/MessageInputTokens.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageInputTokens.swift 3 | // SwiftAnthropic 4 | // 5 | // Created by James Rochabrun on 1/3/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct MessageInputTokens: Decodable { 11 | 12 | /// The total number of tokens across the provided list of messages, system prompt, and tools. 13 | public let inputTokens: Int 14 | 15 | public init(from decoder: Decoder) throws { 16 | if let container = try? decoder.singleValueContainer(), 17 | let dict = try? container.decode([String: Int].self), 18 | let tokens = dict["input_tokens"] { 19 | self.inputTokens = tokens 20 | } else { 21 | // Try regular JSON decoding as fallback 22 | let container = try decoder.container(keyedBy: CodingKeys.self) 23 | self.inputTokens = try container.decode(Int.self, forKey: .inputTokens) 24 | } 25 | } 26 | 27 | private enum CodingKeys: String, CodingKey { 28 | case inputTokens = "input_tokens" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExampleUITests/SwiftAnthropicExampleUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftAnthropicExampleUITestsLaunchTests.swift 3 | // SwiftAnthropicExampleUITests 4 | // 5 | // Created by James Rochabrun on 2/24/24. 6 | // 7 | 8 | import XCTest 9 | 10 | final class SwiftAnthropicExampleUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Anthropic/Public/ResponseModels/TextCompletion/TextCompletionResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextCompletionResponse.swift 3 | // 4 | // 5 | // Created by James Rochabrun on 1/28/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// [Completion Response](https://docs.anthropic.com/claude/reference/complete_post) 11 | public struct TextCompletionResponse: Decodable { 12 | 13 | /// Unique object identifier. 14 | /// 15 | /// The format and length of IDs may change over time. 16 | public let id: String 17 | 18 | public let type: String 19 | 20 | /// The resulting completion up to and excluding the stop sequences. 21 | public let completion: String 22 | 23 | /// The reason that we stopped. 24 | /// 25 | /// This may be one the following values: 26 | /// 27 | /// - "stop_sequence": we reached a stop sequence — either provided by you via the stop_sequences parameter, 28 | /// or a stop sequence built into the model 29 | /// 30 | /// - "max_tokens": we exceeded max_tokens_to_sample or the model's maximum 31 | public let stopReason: String 32 | 33 | /// The model that handled the request. 34 | public let model: String 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Anthropic/Private/Network/AnthropicAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnthropicAPI.swift 3 | // 4 | // 5 | // Created by James Rochabrun on 1/28/24. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: AnthropicAPI 11 | 12 | struct AnthropicAPI { 13 | 14 | let base: String 15 | let apiPath: APIPath 16 | 17 | enum APIPath { 18 | case messages 19 | case textCompletions 20 | case countTokens 21 | 22 | // Skills endpoints 23 | case skills 24 | case skill(id: String) 25 | case skillVersions(skillId: String) 26 | case skillVersion(skillId: String, version: String) 27 | } 28 | } 29 | 30 | // MARK: AnthropicAPI+Endpoint 31 | 32 | extension AnthropicAPI: Endpoint { 33 | 34 | var path: String { 35 | switch apiPath { 36 | case .messages: return "/v1/messages" 37 | case .countTokens: return "/v1/messages/count_tokens" 38 | case .textCompletions: return "/v1/complete" 39 | 40 | // Skills endpoints 41 | case .skills: return "/v1/skills" 42 | case .skill(let id): return "/v1/skills/\(id)" 43 | case .skillVersions(let skillId): return "/v1/skills/\(skillId)/versions" 44 | case .skillVersion(let skillId, let version): return "/v1/skills/\(skillId)/versions/\(version)" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftAnthropic", 8 | platforms: [ 9 | .iOS(.v15), 10 | .macOS(.v12) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, making them visible to other packages. 14 | .library( 15 | name: "SwiftAnthropic", 16 | targets: ["SwiftAnthropic"]), 17 | ], 18 | dependencies: [ 19 | .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.25.2"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package, defining a module or a test suite. 23 | // Targets can depend on other targets in this package and products from dependencies. 24 | .target( 25 | name: "SwiftAnthropic", 26 | dependencies: [ 27 | .product(name: "AsyncHTTPClient", package: "async-http-client", condition: .when(platforms: [.linux])), 28 | ], 29 | path: "Sources/Anthropic"), 30 | .testTarget( 31 | name: "SwiftAnthropicTests", 32 | dependencies: ["SwiftAnthropic"]), 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExampleTests/SwiftAnthropicExampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftAnthropicExampleTests.swift 3 | // SwiftAnthropicExampleTests 4 | // 5 | // Created by James Rochabrun on 2/24/24. 6 | // 7 | 8 | import XCTest 9 | @testable import SwiftAnthropicExample 10 | 11 | final class SwiftAnthropicExampleTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | } 28 | 29 | func testPerformanceExample() throws { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Anthropic/Public/Parameters/Message/MessageTokenCountParameter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageTokenCountParameter.swift 3 | // SwiftAnthropic 4 | // 5 | // Created by James Rochabrun on 1/3/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct MessageTokenCountParameter: Encodable { 11 | 12 | /// The model that will complete your prompt. 13 | /// See [models](https://docs.anthropic.com/claude/reference/selecting-a-model) for additional details and options. 14 | public let model: String 15 | 16 | /// Input messages. 17 | /// Our models are trained to operate on alternating user and assistant conversational turns. 18 | /// Each input message must be an object with a role and content. 19 | public let messages: [MessageParameter.Message] 20 | 21 | /// System prompt. 22 | /// A system prompt is a way of providing context and instructions to Claude. 23 | /// System role can be either a simple String or an array of objects, use the objects array for prompt caching. 24 | public let system: MessageParameter.System? 25 | 26 | /// Tools that can be used in the messages 27 | public let tools: [MessageParameter.Tool]? 28 | 29 | public init( 30 | model: Model, 31 | messages: [MessageParameter.Message], 32 | system: MessageParameter.System? = nil, 33 | tools: [MessageParameter.Tool]? = nil) 34 | { 35 | self.model = model.value 36 | self.messages = messages 37 | self.system = system 38 | self.tools = tools 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExample/ServiceSelectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceSelectionView.swift 3 | // SwiftAnthropicExample 4 | // 5 | // Created by Lou Zell on 7/31/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ServiceSelectionView: View { 11 | 12 | var body: some View { 13 | NavigationStack { 14 | List { 15 | Section("Select Service") { 16 | NavigationLink(destination: ApiKeyIntroView()) { 17 | VStack(alignment: .leading) { 18 | Text("Default Anthropic Service") 19 | .padding(.bottom, 10) 20 | Group { 21 | Text("Use this service to test Anthropic functionality by providing your own Anthropic key.") 22 | } 23 | .font(.caption) 24 | .fontWeight(.light) 25 | } 26 | } 27 | 28 | NavigationLink(destination: AIProxyIntroView()) { 29 | VStack(alignment: .leading) { 30 | Text("AIProxy Service") 31 | .padding(.bottom, 10) 32 | Group { 33 | Text("Use this service to test Anthropic functionality with requests proxied through AIProxy for key protection.") 34 | } 35 | .font(.caption) 36 | .fontWeight(.light) 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | 45 | #Preview { 46 | ServiceSelectionView() 47 | } 48 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExampleUITests/SwiftAnthropicExampleUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftAnthropicExampleUITests.swift 3 | // SwiftAnthropicExampleUITests 4 | // 5 | // Created by James Rochabrun on 2/24/24. 6 | // 7 | 8 | import XCTest 9 | 10 | final class SwiftAnthropicExampleUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | func testLaunchPerformance() throws { 34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 35 | // This measures how long it takes to launch your application. 36 | measure(metrics: [XCTApplicationLaunchMetric()]) { 37 | XCUIApplication().launch() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExample/OptionsListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionsListView.swift 3 | // SwiftAnthropicExample 4 | // 5 | // Created by James Rochabrun on 2/24/24. 6 | // 7 | 8 | import Foundation 9 | import SwiftAnthropic 10 | import SwiftUI 11 | 12 | struct OptionsListView: View { 13 | 14 | let service: AnthropicService 15 | 16 | @State private var selection: APIOption? = nil 17 | 18 | /// https://docs.anthropic.com/claude/reference/getting-started-with-the-api 19 | enum APIOption: String, CaseIterable, Identifiable { 20 | 21 | case message = "Message" 22 | case messageFunctionCall = "Function Call" 23 | case thinking = "Thinking Mode" 24 | case skills = "Skills API" 25 | 26 | var id: Self { self } 27 | } 28 | 29 | var body: some View { 30 | List(APIOption.allCases, id: \.self, selection: $selection) { option in 31 | Text(option.rawValue) 32 | } 33 | .sheet(item: $selection) { selection in 34 | VStack { 35 | Text(selection.rawValue) 36 | .font(.largeTitle) 37 | .padding() 38 | switch selection { 39 | case .message: 40 | MessageDemoView(observable: .init(service: service)) 41 | case .messageFunctionCall: 42 | MessageFunctionCallingDemoView(observable: .init(service: service)) 43 | case .thinking: 44 | ThinkingModeMessageDemoView(observable: .init(service: service)) 45 | case .skills: 46 | SkillsDemoView(observable: .init(service: service)) 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExample/ApiKeyIntroView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiKeyIntroView.swift 3 | // SwiftAnthropicExample 4 | // 5 | // Created by James Rochabrun on 2/24/24. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftAnthropic 10 | 11 | struct ApiKeyIntroView: View { 12 | 13 | @State private var apiKey = "" 14 | 15 | var body: some View { 16 | NavigationStack { 17 | VStack { 18 | Spacer() 19 | VStack(spacing: 24) { 20 | TextField("Enter API Key", text: $apiKey) 21 | } 22 | .padding() 23 | .textFieldStyle(.roundedBorder) 24 | NavigationLink(destination: OptionsListView(service: AnthropicServiceFactory.service( 25 | apiKey: apiKey, 26 | betaHeaders: ["prompt-caching-2024-07-31", "max-tokens-3-5-sonnet-2024-07-15", "skills-2025-10-02", "code-execution-2025-08-25"], debugEnabled: true))) { 27 | Text("Continue") 28 | .padding() 29 | .padding(.horizontal, 48) 30 | .foregroundColor(.white) 31 | .background( 32 | Capsule() 33 | .foregroundColor(apiKey.isEmpty ? .gray.opacity(0.2) : Color(red: 186/255, green: 91/255, blue: 55/255))) 34 | } 35 | .disabled(apiKey.isEmpty) 36 | Spacer() 37 | Group { 38 | Text("You can find a blog post in how to use the `SwiftAnthropic` Package ") + Text("[here](https://medium.com/@jamesrochabrun/anthropic-ios-sdk-032e1dc6afd8)") 39 | Text("If you don't have a valid API KEY yet, you can visit ") + Text("[this link](https://www.anthropic.com/earlyaccess)") + Text(" to get started.") 40 | } 41 | .font(.caption) 42 | } 43 | .padding() 44 | .navigationTitle("Enter Anthropic API KEY") 45 | } 46 | } 47 | } 48 | 49 | #Preview { 50 | ApiKeyIntroView() 51 | } 52 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExample/PhotoPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoPicker.swift 3 | // SwiftAnthropicExample 4 | // 5 | // Created by James Rochabrun on 3/4/24. 6 | // 7 | 8 | import PhotosUI 9 | import SwiftUI 10 | 11 | // MARK: PhotoPicker 12 | 13 | struct PhotoPicker: View { 14 | 15 | @State private var selectedItems: [PhotosPickerItem] = [] 16 | @Binding private var selectedImageURLS: [URL] 17 | @Binding private var selectedImages: [Image] 18 | 19 | init( 20 | selectedImageURLS: Binding<[URL]>, 21 | selectedImages: Binding<[Image]>) 22 | { 23 | _selectedImageURLS = selectedImageURLS 24 | _selectedImages = selectedImages 25 | } 26 | 27 | var body: some View { 28 | PhotosPicker(selection: $selectedItems, matching: .images) { 29 | Image(systemName: "photo") 30 | } 31 | .onChange(of: selectedItems) { 32 | Task { 33 | selectedImages.removeAll() 34 | for item in selectedItems { 35 | if let data = try? await item.loadTransferable(type: Data.self) { 36 | let base64String = data.base64EncodedString() 37 | let url = URL(string: "data:image/jpeg;base64,\(base64String)")! 38 | selectedImageURLS.append(url) 39 | #if canImport(UIKit) 40 | if let uiImage = UIImage(data: data) { 41 | let image = Image(uiImage: uiImage) 42 | selectedImages.append(image) 43 | } 44 | #elseif canImport(AppKit) 45 | if let uiImage = NSImage(data: data) { 46 | let image = Image(nsImage: uiImage) 47 | selectedImages.append(image) 48 | } 49 | #endif 50 | 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | #Preview { 59 | PhotoPicker(selectedImageURLS: .constant([]), selectedImages: .constant([Image(systemName: "photo")])) 60 | } 61 | -------------------------------------------------------------------------------- /Sources/Anthropic/Public/ResponseModels/ErrorResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorResponse.swift 3 | // 4 | // 5 | // Created by James Rochabrun on 1/28/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /* 11 | HTTP errors 12 | Our API follows a predictable HTTP error code format: 13 | 14 | 400 - Invalid request: there was an issue with the format or content of your request. 15 | 401 - Unauthorized: there's an issue with your API key. 16 | 403 - Forbidden: your API key does not have permission to use the specified resource. 17 | 404 - Not found: the requested resource was not found. 18 | 429 - Your account has hit a rate limit. 19 | 500 - An unexpected error has occurred internal to Anthropic's systems. 20 | 529 - Anthropic's API is temporarily overloaded. 21 | When receiving a streaming response via SSE, it's possible that an error can occur after returning a 200 response, in which case error handling wouldn't follow these standard mechanisms. 22 | 23 | Error shapes 24 | Errors are always returned as JSON, with a top-level error object that always includes a type and message value. For example: 25 | 26 | ```JSON 27 | 28 | { 29 | "type": "error", 30 | "error": { 31 | "type": "not_found_error", 32 | "message": "The requested resource could not be found." 33 | } 34 | } 35 | ``` 36 | 37 | In accordance with our versioning policy, we may expand the values within these objects, and it is possible that the type values will grow over time. 38 | 39 | Rate limits 40 | Our rate limits are currently measured in number of concurrent requests across your organization, and will default to 1 while you’re evaluating the API. This means that your organization can make at most 1 request at a time to our API. 41 | 42 | If you exceed the rate limit you will get a 429 error. Once you’re ready to go live we’ll discuss the appropriate rate limit with you. 43 | */ 44 | 45 | struct ErrorResponse: Decodable { 46 | 47 | let type: String 48 | let error: Error 49 | 50 | struct Error: Decodable { 51 | 52 | let type: String 53 | 54 | let message: String 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /Sources/Anthropic/Public/ResponseModels/Message/MessageResponse+DynamicContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageResponse+DynamicContent.swift 3 | // SwiftAnthropic 4 | // 5 | // Created by James Rochabrun on 5/22/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Dictionary where Key == String, Value == MessageResponse.Content.DynamicContent { 11 | 12 | /// Creates a formatted string representation of the dictionary 13 | /// - Parameters: 14 | /// - indent: The indentation level (default: 0) 15 | /// - indentSize: Number of spaces per indent level (default: 2) 16 | /// - Returns: A formatted string representation 17 | func formattedDescription(indent: Int = 0, indentSize: Int = 2) -> String { 18 | let indentation = String(repeating: " ", count: indent * indentSize) 19 | let nextIndent = indent + 1 20 | 21 | return self.map { key, value in 22 | let valueStr = formatValue(value, indent: nextIndent, indentSize: indentSize) 23 | return "\(indentation)\(key): \(valueStr)" 24 | }.joined(separator: "\n") 25 | } 26 | 27 | /// Formats a single DynamicContent value 28 | private func formatValue(_ value: MessageResponse.Content.DynamicContent, indent: Int, indentSize: Int) -> String { 29 | let indentation = String(repeating: " ", count: indent * indentSize) 30 | 31 | switch value { 32 | case .string(let str): 33 | return "\"\(str)\"" 34 | 35 | case .integer(let num): 36 | return "\(num)" 37 | 38 | case .double(let num): 39 | return "\(num)" 40 | 41 | case .bool(let bool): 42 | return "\(bool)" 43 | 44 | case .null: 45 | return "null" 46 | 47 | case .dictionary(let dict): 48 | let dictStr = dict.formattedDescription(indent: indent + 1, indentSize: indentSize) 49 | return "{\n\(dictStr)\n\(indentation)}" 50 | 51 | case .array(let arr): 52 | if arr.isEmpty { 53 | return "[]" 54 | } 55 | 56 | let items = arr.enumerated().map { index, item in 57 | let itemStr = formatValue(item, indent: indent + 1, indentSize: indentSize) 58 | return "\(indentation) [\(index)]: \(itemStr)" 59 | }.joined(separator: "\n") 60 | 61 | return "[\n\(items)\n\(indentation)]" 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExample/AIProxyIntroView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AIProxyIntroView.swift 3 | // SwiftAnthropicExample 4 | // 5 | // Created by Lou Zell on 7/31/24. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftAnthropic 10 | 11 | struct AIProxyIntroView: View { 12 | 13 | @State private var partialKey = "" 14 | @State private var serviceURL = "" 15 | 16 | private var canProceed: Bool { 17 | return !(self.partialKey.isEmpty || self.serviceURL.isEmpty) 18 | } 19 | 20 | var body: some View { 21 | NavigationStack { 22 | VStack { 23 | Spacer() 24 | VStack(spacing: 24) { 25 | TextField("Enter partial key", text: $partialKey) 26 | TextField("Enter your service's URL", text: $serviceURL) 27 | } 28 | .padding() 29 | .textFieldStyle(.roundedBorder) 30 | 31 | Text("You receive a partial key and service URL when you configure an app in the AIProxy dashboard") 32 | .font(.caption) 33 | 34 | NavigationLink(destination: OptionsListView(service: aiproxyService)) { 35 | Text("Continue") 36 | .padding() 37 | .padding(.horizontal, 48) 38 | .foregroundColor(.white) 39 | .background( 40 | Capsule() 41 | .foregroundColor(canProceed ? Color(red: 186/255, green: 91/255, blue: 55/255) : .gray.opacity(0.2))) 42 | } 43 | .disabled(!canProceed) 44 | Spacer() 45 | Group { 46 | Text("AIProxy keeps your Anthropic API key secure. To configure AIProxy for your project, or to learn more about how it works, please see the docs at ") + Text("[this link](https://www.aiproxy.pro/docs).") 47 | } 48 | .font(.caption) 49 | } 50 | .padding() 51 | .navigationTitle("AIProxy Configuration") 52 | } 53 | } 54 | 55 | private var aiproxyService: AnthropicService { 56 | return AnthropicServiceFactory.service( 57 | aiproxyPartialKey: partialKey, 58 | aiproxyServiceURL: serviceURL, 59 | betaHeaders: ["max-tokens-3-5-sonnet-2024-07-15", "prompt-caching-2024-07-31"] 60 | ) 61 | } 62 | } 63 | 64 | #Preview { 65 | ApiKeyIntroView() 66 | } 67 | -------------------------------------------------------------------------------- /Sources/Anthropic/Public/Model.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Model.swift 3 | // 4 | // 5 | // Created by James Rochabrun on 2/24/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Currently available models 11 | /// We currently offer two families of models: 12 | /// 13 | /// - Claude Instant: low-latency, high throughput. 14 | /// - Claude: superior performance on tasks that require complex reasoning. 15 | /// 16 | /// See our pricing page for pricing details. 17 | /// 18 | /// When making requests to APIs, you must specify the model to perform the completion using the model parameter. 19 | /// 20 | /// API model name Model family 21 | /// - claude-instant-1.2 Claude Instant 22 | /// - claude-2.1 Claude 23 | /// - claude-2.0 Claude 24 | /// 25 | /// Anthropic offer two families of models: 26 | /// 27 | /// *Claude Instant:* low-latency, high throughput. 28 | /// *Claude:* superior performance on tasks that require complex reasoning. 29 | /// 30 | /// When making requests to APIs, you must specify the model to perform the completion using the model parameter. 31 | /// 32 | /// Family Latest version 33 | /// Claude Instant claude-instant-1.2 34 | /// Claude claude-2.1 35 | /// Note that we previously supported specifying only the major version number, e.g., claude-2, which would result in new minor versions being used automatically as they are released. However, we no longer recommend this integration pattern, and the new Messages API does not support it. 36 | /// 37 | /// Each model has a maximum total context window size and a maximum completion length. 38 | /// 39 | /// Model Context window size Max completion length 40 | /// claude-2.1 200,000 tokens 4,096 tokens 41 | /// claude-2.0 100,000 tokens 4,096 tokens 42 | /// claude-instant-1.2 100,000 tokens 4,096 tokens 43 | /// The total context window size includes both the request prompt length and response completion length. If the prompt length approaches the context window size, the max output length will be reduced to fit within the context window size. 44 | /// 45 | /// If you encounter "stop_reason": "max_tokens" in a completion response and want Claude to continue from where it left off, you can make a new request with the previous completion appended to the previous prompt. 46 | 47 | /// [More](https://docs.anthropic.com/claude/reference/selecting-a-model) 48 | /// [Models](https://docs.anthropic.com/en/docs/about-claude/models) 49 | public enum Model { 50 | 51 | case claudeInstant12 52 | case claude2 53 | case claude21 54 | case claude3Opus 55 | case claude3Sonnet 56 | case claude35Sonnet 57 | case claude3Haiku 58 | case claude35Haiku 59 | case claude37Sonnet 60 | 61 | case other(String) 62 | 63 | public var value: String { 64 | switch self { 65 | case .claudeInstant12: return "claude-instant-1.2" 66 | case .claude2: return "claude-2.0" 67 | case .claude21: return "claude-2.1" 68 | case .claude3Opus: return "claude-3-opus-20240229" 69 | case .claude3Sonnet: return "claude-3-sonnet-20240229" 70 | case .claude35Sonnet: return "claude-3-5-sonnet-latest" 71 | case .claude3Haiku: return "claude-3-haiku-20240307" 72 | case .claude35Haiku: return "claude-3-5-haiku-latest" 73 | case .claude37Sonnet: return "claude-3-7-sonnet-latest" 74 | case .other(let model): return model 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/Anthropic/Public/Parameters/Message/MessageParameter+Web.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageParameter+Web.swift 3 | // SwiftAnthropic 4 | // 5 | // Created by James Rochabrun on 5/12/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension MessageParameter { 11 | 12 | /// Creates a web search tool with default configuration 13 | static func webSearch( 14 | name: String = "web_search", 15 | maxUses: Int? = nil 16 | ) -> Tool { 17 | let parameters = WebSearchParameters(maxUses: maxUses) 18 | return .webSearch(name: name, parameters: parameters) 19 | } 20 | 21 | /// Creates a web search tool with domain filtering 22 | static func webSearch( 23 | name: String = "web_search", 24 | maxUses: Int? = nil, 25 | allowedDomains: [String]? = nil, 26 | blockedDomains: [String]? = nil 27 | ) -> Tool { 28 | let parameters = WebSearchParameters( 29 | maxUses: maxUses, 30 | allowedDomains: allowedDomains, 31 | blockedDomains: blockedDomains 32 | ) 33 | return .webSearch(name: name, parameters: parameters) 34 | } 35 | 36 | /// Creates a web search tool with user location for localized results 37 | static func webSearch( 38 | name: String = "web_search", 39 | maxUses: Int? = nil, 40 | userLocation: UserLocation 41 | ) -> Tool { 42 | let parameters = WebSearchParameters( 43 | maxUses: maxUses, 44 | userLocation: userLocation 45 | ) 46 | return .webSearch(name: name, parameters: parameters) 47 | } 48 | 49 | /// Creates a web search tool with full configuration 50 | static func webSearch( 51 | name: String = "web_search", 52 | maxUses: Int? = nil, 53 | allowedDomains: [String]? = nil, 54 | blockedDomains: [String]? = nil, 55 | userLocation: UserLocation? = nil 56 | ) -> Tool { 57 | let parameters = WebSearchParameters( 58 | maxUses: maxUses, 59 | allowedDomains: allowedDomains, 60 | blockedDomains: blockedDomains, 61 | userLocation: userLocation 62 | ) 63 | return .webSearch(name: name, parameters: parameters) 64 | } 65 | 66 | /// Creates a location for a US city 67 | static func usCity( 68 | city: String, 69 | region: String, 70 | timezone: String 71 | ) -> UserLocation { 72 | return UserLocation( 73 | type: .approximate, 74 | city: city, 75 | region: region, 76 | country: "US", 77 | timezone: timezone 78 | ) 79 | } 80 | } 81 | 82 | public extension MessageParameter.UserLocation { 83 | 84 | /// Common US locations 85 | static let sanFrancisco = Self( 86 | type: .approximate, 87 | city: "San Francisco", 88 | region: "California", 89 | country: "US", 90 | timezone: "America/Los_Angeles" 91 | ) 92 | 93 | static let newYork = Self( 94 | type: .approximate, 95 | city: "New York", 96 | region: "New York", 97 | country: "US", 98 | timezone: "America/New_York" 99 | ) 100 | 101 | static let chicago = Self( 102 | type: .approximate, 103 | city: "Chicago", 104 | region: "Illinois", 105 | country: "US", 106 | timezone: "America/Chicago" 107 | ) 108 | } 109 | -------------------------------------------------------------------------------- /Sources/Anthropic/Service/AnthropicServiceFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnthropicServiceFactory.swift 3 | // 4 | // 5 | // Created by James Rochabrun on 1/28/24. 6 | // 7 | 8 | import Foundation 9 | #if os(Linux) 10 | import FoundationNetworking 11 | #endif 12 | 13 | 14 | public final class AnthropicServiceFactory { 15 | 16 | /// Creates and returns an instance of `AnthropicService`. 17 | /// 18 | /// - Parameters: 19 | /// - apiKey: The API key required for authentication. 20 | /// - apiVersion: The Anthropic api version. Currently "2023-06-01". (Can be overriden) 21 | /// - basePath: An overridable base path for requests, defaults to https://api.anthropic.com 22 | /// - betaHeaders: An array of headers for Anthropic's beta features. 23 | /// - httpClient: The HTTP client to be used for network calls (default creates platform-appropriate client). 24 | /// - debugEnabled: If `true` service prints event on DEBUG builds, default to `false`. 25 | /// 26 | /// - Returns: A fully configured object conforming to `AnthropicService`. 27 | public static func service( 28 | apiKey: String, 29 | apiVersion: String = "2023-06-01", 30 | basePath: String = "https://api.anthropic.com", 31 | betaHeaders: [String]?, 32 | httpClient: HTTPClient? = nil, 33 | debugEnabled: Bool = false) 34 | -> AnthropicService 35 | { 36 | DefaultAnthropicService( 37 | apiKey: apiKey, 38 | apiVersion: apiVersion, 39 | basePath: basePath, 40 | betaHeaders: betaHeaders, 41 | httpClient: httpClient ?? HTTPClientFactory.createDefault(), 42 | debugEnabled: debugEnabled) 43 | } 44 | 45 | #if !os(Linux) 46 | /// Creates and returns an instance of `AnthropicService`. 47 | /// 48 | /// - Parameters: 49 | /// - aiproxyPartialKey: The partial key provided in the 'API Keys' section of the AIProxy dashboard. 50 | /// Please see the integration guide for acquiring your key, at https://www.aiproxy.pro/docs 51 | /// 52 | /// - aiproxyServiceURL: The service URL is displayed in the AIProxy dashboard when you submit your Anthropic key. 53 | /// 54 | /// - aiproxyClientID: If your app already has client or user IDs that you want to annotate AIProxy requests 55 | /// with, you can pass a clientID here. If you do not have existing client or user IDs, leave 56 | /// the `clientID` argument out, and IDs will be generated automatically for you. 57 | /// 58 | /// - apiVersion: The Anthropic api version. Currently "2023-06-01". (Can be overriden) 59 | /// - betaHeaders: An array of headers for Anthropic's beta features. 60 | /// - debugEnabled: If `true` service prints event on DEBUG builds, default to `false`. 61 | /// 62 | /// - Returns: A conformer of `AnthropicService` that proxies all requests through api.aiproxy.pro 63 | public static func service( 64 | aiproxyPartialKey: String, 65 | aiproxyServiceURL: String, 66 | aiproxyClientID: String? = nil, 67 | apiVersion: String = "2023-06-01", 68 | betaHeaders: [String]?, 69 | debugEnabled: Bool = false) 70 | -> AnthropicService 71 | { 72 | AIProxyService( 73 | partialKey: aiproxyPartialKey, 74 | serviceURL: aiproxyServiceURL, 75 | clientID: aiproxyClientID, 76 | apiVersion: apiVersion, 77 | betaHeaders: betaHeaders, 78 | debugEnabled: debugEnabled) 79 | } 80 | #endif 81 | 82 | } 83 | -------------------------------------------------------------------------------- /Sources/Anthropic/Private/Networking/HTTPClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if canImport(FoundationNetworking) 4 | import FoundationNetworking 5 | #endif 6 | 7 | // MARK: - HTTPClient 8 | 9 | /// Protocol that abstracts HTTP client functionality 10 | public protocol HTTPClient { 11 | /// Fetches data for a given HTTP request 12 | /// - Parameter request: The HTTP request to perform 13 | /// - Returns: A tuple containing the data and HTTP response 14 | func data(for request: HTTPRequest) async throws -> (Data, HTTPResponse) 15 | 16 | /// Fetches a byte stream for a given HTTP request 17 | /// - Parameter request: The HTTP request to perform 18 | /// - Returns: A tuple containing the byte stream and HTTP response 19 | func bytes(for request: HTTPRequest) async throws -> (HTTPByteStream, HTTPResponse) 20 | } 21 | 22 | // MARK: - HTTPRequest 23 | 24 | /// Represents an HTTP request with platform-agnostic properties 25 | public struct HTTPRequest { 26 | public init(url: URL, method: HTTPMethod, headers: [String: String], body: Data? = nil) { 27 | self.url = url 28 | self.method = method 29 | self.headers = headers 30 | self.body = body 31 | } 32 | 33 | /// Initializes an HTTPRequest from a URLRequest 34 | /// - Parameter urlRequest: The URLRequest to convert 35 | public init(from urlRequest: URLRequest) throws { 36 | guard let url = urlRequest.url else { 37 | throw URLError(.badURL) 38 | } 39 | 40 | guard 41 | let httpMethodString = urlRequest.httpMethod, 42 | let httpMethod = HTTPMethod(rawValue: httpMethodString) 43 | else { 44 | throw URLError(.unsupportedURL) 45 | } 46 | 47 | var headers: [String: String] = [:] 48 | if let allHTTPHeaderFields = urlRequest.allHTTPHeaderFields { 49 | headers = allHTTPHeaderFields 50 | } 51 | 52 | self.url = url 53 | method = httpMethod 54 | self.headers = headers 55 | body = urlRequest.httpBody 56 | } 57 | 58 | /// The URL for the request 59 | var url: URL 60 | /// The HTTP method for the request 61 | var method: HTTPMethod 62 | /// The HTTP headers for the request 63 | var headers: [String: String] 64 | /// The body of the request, if any 65 | var body: Data? 66 | 67 | } 68 | 69 | // MARK: - HTTPResponse 70 | 71 | /// Represents an HTTP response with platform-agnostic properties 72 | public struct HTTPResponse { 73 | /// The HTTP status code of the response 74 | var statusCode: Int 75 | /// The HTTP headers in the response 76 | var headers: [String: String] 77 | 78 | public init(statusCode: Int, headers: [String: String]) { 79 | self.statusCode = statusCode 80 | self.headers = headers 81 | } 82 | } 83 | 84 | // MARK: - HTTPByteStream 85 | 86 | /// Represents a stream of bytes or lines from an HTTP response 87 | public enum HTTPByteStream { 88 | /// A stream of bytes 89 | case bytes(AsyncThrowingStream) 90 | /// A stream of lines (strings) 91 | case lines(AsyncThrowingStream) 92 | } 93 | 94 | // MARK: - HTTPClientFactory 95 | 96 | public enum HTTPClientFactory { 97 | /// Creates a default HTTPClient implementation appropriate for the current platform 98 | /// - Returns: URLSessionHTTPClientAdapter on Apple platforms, AsyncHTTPClientAdapter on Linux 99 | public static func createDefault() -> HTTPClient { 100 | #if os(Linux) 101 | return AsyncHTTPClientAdapter.createDefault() 102 | #else 103 | return URLSessionHTTPClientAdapter() 104 | #endif 105 | } 106 | } -------------------------------------------------------------------------------- /Sources/Anthropic/Private/Networking/URLSessionHTTPClientAdapter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if canImport(FoundationNetworking) 4 | import FoundationNetworking 5 | #endif 6 | 7 | #if !os(Linux) 8 | /// Adapter that implements HTTPClient protocol using URLSession 9 | public class URLSessionHTTPClientAdapter: HTTPClient { 10 | 11 | /// Initializes a new URLSessionHTTPClientAdapter with the provided URLSession 12 | /// - Parameter urlSession: The URLSession instance to use. Defaults to `URLSession.shared`. 13 | public init(urlSession: URLSession = .shared) { 14 | self.urlSession = urlSession 15 | } 16 | 17 | /// Fetches data for a given HTTP request 18 | /// - Parameter request: The HTTP request to perform 19 | /// - Returns: A tuple containing the data and HTTP response 20 | public func data(for request: HTTPRequest) async throws -> (Data, HTTPResponse) { 21 | let urlRequest = try createURLRequest(from: request) 22 | 23 | let (data, urlResponse) = try await urlSession.data(for: urlRequest) 24 | 25 | guard let httpURLResponse = urlResponse as? HTTPURLResponse else { 26 | throw URLError(.badServerResponse) // Or a custom error 27 | } 28 | 29 | let response = HTTPResponse( 30 | statusCode: httpURLResponse.statusCode, 31 | headers: convertHeaders(httpURLResponse.allHeaderFields)) 32 | 33 | return (data, response) 34 | } 35 | 36 | /// Fetches a byte stream for a given HTTP request 37 | /// - Parameter request: The HTTP request to perform 38 | /// - Returns: A tuple containing the byte stream and HTTP response 39 | public func bytes(for request: HTTPRequest) async throws -> (HTTPByteStream, HTTPResponse) { 40 | let urlRequest = try createURLRequest(from: request) 41 | 42 | let (asyncBytes, urlResponse) = try await urlSession.bytes(for: urlRequest) 43 | 44 | guard let httpURLResponse = urlResponse as? HTTPURLResponse else { 45 | throw URLError(.badServerResponse) // Or a custom error 46 | } 47 | 48 | let response = HTTPResponse( 49 | statusCode: httpURLResponse.statusCode, 50 | headers: convertHeaders(httpURLResponse.allHeaderFields)) 51 | 52 | let stream = AsyncThrowingStream { continuation in 53 | Task { 54 | do { 55 | for try await line in asyncBytes.lines { 56 | continuation.yield(line) 57 | } 58 | continuation.finish() 59 | } catch { 60 | continuation.finish(throwing: error) 61 | } 62 | } 63 | } 64 | 65 | return (.lines(stream), response) 66 | } 67 | 68 | private let urlSession: URLSession 69 | 70 | /// Converts our HTTPRequest to URLRequest 71 | /// - Parameter request: Our HTTPRequest 72 | /// - Returns: URLRequest 73 | private func createURLRequest(from request: HTTPRequest) throws -> URLRequest { 74 | var urlRequest = URLRequest(url: request.url) 75 | urlRequest.httpMethod = request.method.rawValue 76 | 77 | for (key, value) in request.headers { 78 | urlRequest.setValue(value, forHTTPHeaderField: key) 79 | } 80 | 81 | urlRequest.httpBody = request.body 82 | 83 | return urlRequest 84 | } 85 | 86 | /// Converts HTTPURLResponse headers to a dictionary [String: String] 87 | /// - Parameter headers: The headers from HTTPURLResponse (i.e. `allHeaderFields`) 88 | /// - Returns: Dictionary of header name-value pairs 89 | private func convertHeaders(_ headers: [AnyHashable: Any]) -> [String: String] { 90 | var result = [String: String]() 91 | for (key, value) in headers { 92 | if let keyString = key as? String, let valueString = value as? String { 93 | result[keyString] = valueString 94 | } 95 | } 96 | return result 97 | } 98 | } 99 | #endif -------------------------------------------------------------------------------- /Sources/Anthropic/Public/Parameters/Skill/SkillParameter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkillParameter.swift 3 | // 4 | // 5 | // Created by James Rochabrun on 10/25/25. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Skill Creation Parameters 11 | 12 | /// Parameters for creating a new skill. 13 | /// See [Create Skill API](https://docs.anthropic.com/claude/reference/skills/create-skill) 14 | public struct SkillCreateParameter { 15 | 16 | /// Display title for the skill. 17 | /// This is a human-readable label that is not included in the prompt sent to the model. 18 | public let displayTitle: String? 19 | 20 | /// Files to upload for the skill. 21 | /// All files must be in the same top-level directory and must include a SKILL.md file at the root. 22 | /// Total upload size must be under 8MB. 23 | public let files: [SkillFile] 24 | 25 | public init( 26 | displayTitle: String? = nil, 27 | files: [SkillFile] 28 | ) { 29 | self.displayTitle = displayTitle 30 | self.files = files 31 | } 32 | } 33 | 34 | /// Represents a file to be uploaded as part of a skill 35 | public struct SkillFile { 36 | /// The file name with path relative to skill root (e.g., "skill_name/SKILL.md") 37 | public let filename: String 38 | /// The file data 39 | public let data: Data 40 | /// The MIME type of the file (e.g., "text/markdown", "text/x-python") 41 | public let mimeType: String? 42 | 43 | public init( 44 | filename: String, 45 | data: Data, 46 | mimeType: String? = nil 47 | ) { 48 | self.filename = filename 49 | self.data = data 50 | self.mimeType = mimeType 51 | } 52 | } 53 | 54 | // MARK: - Skill Version Parameters 55 | 56 | /// Parameters for creating a new version of an existing skill. 57 | /// See [Create Skill Version API](https://docs.anthropic.com/claude/reference/skills/create-skill-version) 58 | public struct SkillVersionCreateParameter { 59 | 60 | /// Files to upload for the skill version. 61 | /// All files must be in the same top-level directory and must include a SKILL.md file at the root. 62 | /// Total upload size must be under 8MB. 63 | public let files: [SkillFile] 64 | 65 | public init(files: [SkillFile]) { 66 | self.files = files 67 | } 68 | } 69 | 70 | // MARK: - List Skills Parameters 71 | 72 | /// Parameters for listing skills with optional filtering. 73 | /// See [List Skills API](https://docs.anthropic.com/claude/reference/skills/list-skills) 74 | public struct ListSkillsParameter { 75 | 76 | /// Pagination token for fetching a specific page of results. 77 | /// Pass the value from a previous response's `nextPage` field to get the next page. 78 | public let page: String? 79 | 80 | /// Number of results to return per page. 81 | /// Maximum value is 100. Defaults to 20. 82 | public let limit: Int? 83 | 84 | /// Filter skills by source. 85 | /// - "custom": only return user-created skills 86 | /// - "anthropic": only return Anthropic-created skills 87 | public let source: SkillSource? 88 | 89 | public enum SkillSource: String { 90 | case custom 91 | case anthropic 92 | } 93 | 94 | public init( 95 | page: String? = nil, 96 | limit: Int? = nil, 97 | source: SkillSource? = nil 98 | ) { 99 | self.page = page 100 | self.limit = limit 101 | self.source = source 102 | } 103 | } 104 | 105 | // MARK: - List Skill Versions Parameters 106 | 107 | /// Parameters for listing versions of a specific skill. 108 | /// See [List Skill Versions API](https://docs.anthropic.com/claude/reference/skills/list-skill-versions) 109 | public struct ListSkillVersionsParameter { 110 | 111 | /// Pagination token for fetching a specific page of results. 112 | public let page: String? 113 | 114 | /// Number of results to return per page. 115 | /// Maximum value is 100. Defaults to 20. 116 | public let limit: Int? 117 | 118 | public init( 119 | page: String? = nil, 120 | limit: Int? = nil 121 | ) { 122 | self.page = page 123 | self.limit = limit 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Sources/Anthropic/Public/ResponseModels/Skill/SkillResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkillResponse.swift 3 | // 4 | // 5 | // Created by James Rochabrun on 10/25/25. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Skill Response 11 | 12 | /// Response for a single skill. 13 | /// Returned by create, retrieve, and list operations. 14 | /// See [Skills API](https://docs.anthropic.com/claude/reference/skills) 15 | public struct SkillResponse: Decodable { 16 | 17 | /// Unique identifier for the skill. 18 | /// The format and length of IDs may change over time. 19 | /// - Example: "skill_01JAbcdefghijklmnopqrstuvw" 20 | public let id: String 21 | 22 | /// Object type. For Skills, this is always "skill". 23 | public let type: String 24 | 25 | /// Display title for the skill. 26 | /// This is a human-readable label that is not included in the prompt sent to the model. 27 | public let displayTitle: String? 28 | 29 | /// Source of the skill. 30 | /// - "custom": the skill was created by a user 31 | /// - "anthropic": the skill was created by Anthropic 32 | public let source: String 33 | 34 | /// The latest version identifier for the skill. 35 | /// This represents the most recent version of the skill that has been created. 36 | /// - For Anthropic skills: date-based like "20251013" 37 | /// - For custom skills: epoch timestamp like "1759178010641129" 38 | public let latestVersion: String? 39 | 40 | /// ISO 8601 timestamp of when the skill was created. 41 | /// - Example: "2024-10-30T23:58:27.427722Z" 42 | public let createdAt: String 43 | 44 | /// ISO 8601 timestamp of when the skill was last updated. 45 | /// - Example: "2024-10-30T23:58:27.427722Z" 46 | public let updatedAt: String 47 | } 48 | 49 | // MARK: - List Skills Response 50 | 51 | /// Response for listing skills with pagination support. 52 | /// See [List Skills API](https://docs.anthropic.com/claude/reference/skills/list-skills) 53 | public struct ListSkillsResponse: Decodable { 54 | 55 | /// List of skills. 56 | public let data: [SkillResponse] 57 | 58 | /// Whether there are more results available. 59 | /// If `true`, there are additional results that can be fetched using the `nextPage` token. 60 | public let hasMore: Bool 61 | 62 | /// Token for fetching the next page of results. 63 | /// If `null`, there are no more results available. 64 | /// Pass this value to the `page` parameter in the next request to get the next page. 65 | public let nextPage: String? 66 | } 67 | 68 | // MARK: - Skill Version Response 69 | 70 | /// Response for a skill version. 71 | /// Returned by version create, retrieve, and list operations. 72 | /// See [Skill Versions API](https://docs.anthropic.com/claude/reference/skills/versions) 73 | public struct SkillVersionResponse: Decodable { 74 | 75 | /// Unique identifier for the skill. 76 | public let id: String 77 | 78 | /// Object type. For skill versions, this is always "skill_version". 79 | public let type: String 80 | 81 | /// Display title for the skill. 82 | public let displayTitle: String? 83 | 84 | /// Source of the skill ("custom" or "anthropic"). 85 | public let source: String 86 | 87 | /// Version identifier for this specific version. 88 | /// - For Anthropic skills: date-based like "20251013" 89 | /// - For custom skills: epoch timestamp like "1759178010641129" 90 | public let version: String 91 | 92 | /// ISO 8601 timestamp of when the skill version was created. 93 | public let createdAt: String 94 | } 95 | 96 | // MARK: - List Skill Versions Response 97 | 98 | /// Response for listing skill versions with pagination support. 99 | /// See [List Skill Versions API](https://docs.anthropic.com/claude/reference/skills/list-skill-versions) 100 | public struct ListSkillVersionsResponse: Decodable { 101 | 102 | /// List of skill versions. 103 | public let data: [SkillVersionResponse] 104 | 105 | /// Whether there are more results available. 106 | public let hasMore: Bool 107 | 108 | /// Token for fetching the next page of results. 109 | public let nextPage: String? 110 | } 111 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExample/FunctionCalling/MessageFunctionCallingObservable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageFunctionCallingObservable.swift 3 | // 4 | // 5 | // Created by James Rochabrun on 4/4/24. 6 | // 7 | 8 | import Foundation 9 | import SwiftAnthropic 10 | import SwiftUI 11 | 12 | @MainActor 13 | @Observable class MessageFunctionCallingObservable { 14 | 15 | let service: AnthropicService 16 | var errorMessage = "" 17 | var isLoading = false 18 | var message = "" 19 | var thinking = "" 20 | 21 | var toolUse: MessageResponse.Content.ToolUse? 22 | 23 | // Stream tool use response 24 | var totalJson: String = "" 25 | 26 | init(service: AnthropicService) { 27 | self.service = service 28 | } 29 | 30 | func createMessage( 31 | parameters: MessageParameter) async throws 32 | { 33 | task = Task { 34 | do { 35 | isLoading = true 36 | let message = try await service.createMessage(parameters) 37 | isLoading = false 38 | for content in message.content { 39 | switch content { 40 | case .text(let text, _): 41 | self.message = text 42 | case .toolUse(let toolUSe): 43 | toolUse = toolUSe 44 | case .thinking(let thinking): 45 | self.thinking = thinking.thinking 46 | case .serverToolUse(let serverToolUse): 47 | dump(serverToolUse) 48 | case .webSearchToolResult(let webSearchTool): 49 | dump(webSearchTool) 50 | case .toolResult(let toolResult): 51 | dump(toolResult) 52 | case .codeExecutionToolResult(let toolResult): 53 | dump(toolResult) 54 | } 55 | } 56 | } catch { 57 | self.errorMessage = "\(error)" 58 | } 59 | } 60 | } 61 | 62 | func streamMessage( 63 | parameters: MessageParameter) async throws 64 | { 65 | task = Task { 66 | do { 67 | isLoading = true 68 | let stream = try await service.streamMessage(parameters) 69 | isLoading = false 70 | for try await result in stream { 71 | 72 | let content = result.delta?.text ?? "" 73 | self.message += content 74 | 75 | /// PartialJson is the JSON provided by tool use. Clients need to accumulate it. 76 | /// https://docs.anthropic.com/en/api/messages-streaming#input-json-delta 77 | self.totalJson += result.delta?.partialJson ?? "" 78 | 79 | switch result.streamEvent { 80 | case .contentBlockStart: 81 | // Tool use data is only available in `content_block_start` events. 82 | /* 83 | event: content_block_start 84 | data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01KXkhDdRhvV1pnk23GiWmjo","name":"get_weather","input":{}} } 85 | */ 86 | self.toolUse = result.contentBlock?.toolUse 87 | default: break 88 | } 89 | } 90 | } catch { 91 | self.errorMessage = "\(error)" 92 | } 93 | } 94 | } 95 | 96 | func cancelStream() { 97 | task?.cancel() 98 | } 99 | 100 | func clearMessage() { 101 | message = "" 102 | toolUse = nil 103 | totalJson = "" 104 | } 105 | 106 | // MARK: Private 107 | 108 | private var task: Task? = nil 109 | 110 | } 111 | 112 | extension MessageResponse.Content.ToolUse { 113 | 114 | var inputDisplay: String { 115 | var display = "" 116 | for key in input.keys { 117 | display += key 118 | display += "," 119 | switch input[key] { 120 | case .string(let text): 121 | display += text 122 | default: break 123 | } 124 | } 125 | return display 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Sources/Anthropic/Public/Parameters/TextCompletion/TextCompletionParameter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextCompletionParameter.swift 3 | // 4 | // 5 | // Created by James Rochabrun on 1/28/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// [Create a Text Completion](https://docs.anthropic.com/claude/reference/complete_post) 11 | /// POST: https://api.anthropic.com/v1/complete 12 | public struct TextCompletionParameter: Encodable { 13 | 14 | /// The model that will complete your prompt. 15 | /// As we improve Claude, we develop new versions of it that you can query. The model parameter controls which version of Claude responds 16 | /// to your request. Right now we offer two model families: Claude, and Claude Instant. You can use them by setting model to "claude-2.1" or "claude-instant-1.2", respectively. 17 | /// See [models](https://docs.anthropic.com/claude/reference/selecting-a-model) for additional details and options. 18 | public let model: String 19 | 20 | /// The prompt that you want Claude to complete. 21 | /// 22 | /// For proper response generation you will need to format your prompt using alternating \n\nHuman: and \n\nAssistant: conversational turns. For example: 23 | /// ``` 24 | /// "\n\nHuman: {userQuestion}\n\nAssistant:"` 25 | /// ``` 26 | /// 27 | /// See [prompt validation](https://anthropic.readme.io/claude/reference/prompt-validation) and our guide to [prompt design](https://docs.anthropic.com/claude/docs/introduction-to-prompt-designhttps://docs.anthropic.com/claude/docs/introduction-to-prompt-design) for more details. 28 | public let prompt: String 29 | 30 | /// The maximum number of tokens to generate before stopping. 31 | /// Note that our models may stop before reaching this maximum. This parameter only specifies the absolute maximum number of tokens to generate. 32 | public let maxTokensToSample: Int 33 | 34 | /// Sequences that will cause the model to stop generating. 35 | /// Our models stop on `\n\nHuman:`, and may include additional built-in stop sequences in the future. By providing the stop_sequences parameter, you may include additional strings that will cause the model to stop generating. 36 | public let stopSequences: [String]? 37 | 38 | /// Amount of randomness injected into the response. 39 | /// 40 | /// Defaults to 1. Ranges from 0 to 1. Use temp closer to 0 for analytical / multiple choice, and closer to 1 for creative and generative tasks. 41 | public let temperature: Double? 42 | 43 | /// Use nucleus sampling. 44 | /// 45 | /// In nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and 46 | /// cut it off once it reaches a particular probability specified by top_p. You should either alter temperature or top_p, but not both. 47 | public let topP: Int? 48 | 49 | /// Only sample from the top K options for each subsequent token. 50 | /// 51 | /// Used to remove "long tail" low probability responses. [Learn more technical details here](https://towardsdatascience.com/how-to-sample-from-language-models-682bceb97277). 52 | public let topK: Int? 53 | 54 | /// An object describing metadata about the request. 55 | public let metadata: MetaData? 56 | 57 | /// Whether to incrementally stream the response using server-sent events. 58 | /// See [streaming](https://docs.anthropic.com/claude/reference/text-completions-streaming) for details. 59 | public var stream: Bool 60 | 61 | public struct MetaData: Encodable { 62 | /// An external identifier for the user who is associated with the request. 63 | /// This should be a uuid, hash value, or other opaque identifier. Anthropic may use this id to help detect abuse. Do not include any identifying information such as name, email address, or phone number. 64 | public let userId: UUID 65 | } 66 | 67 | public init( 68 | model: Model, 69 | prompt: String, 70 | maxTokensToSample: Int, 71 | stopSequences: [String]? = nil, 72 | temperature: Double? = nil, 73 | topP: Int? = nil, 74 | topK: Int? = nil, 75 | metadata: MetaData? = nil, 76 | stream: Bool = false) 77 | { 78 | self.model = model.value 79 | self.prompt = prompt 80 | self.maxTokensToSample = maxTokensToSample 81 | self.stopSequences = stopSequences 82 | self.temperature = temperature 83 | self.topP = topP 84 | self.topK = topK 85 | self.metadata = metadata 86 | self.stream = stream 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/Anthropic/Private/Network/Endpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint.swift 3 | // 4 | // 5 | // Created by James Rochabrun on 1/28/24. 6 | // 7 | 8 | import Foundation 9 | #if canImport(FoundationNetworking) 10 | import FoundationNetworking 11 | #endif 12 | 13 | // MARK: HTTPMethod 14 | 15 | public enum HTTPMethod: String { 16 | case post = "POST" 17 | case get = "GET" 18 | case delete = "DELETE" 19 | } 20 | 21 | // MARK: Endpoint 22 | 23 | protocol Endpoint { 24 | 25 | var base: String { get } 26 | var path: String { get } 27 | } 28 | 29 | // MARK: Endpoint+Requests 30 | 31 | extension Endpoint { 32 | 33 | func urlComponents( 34 | queryItems: [URLQueryItem]) 35 | -> URLComponents 36 | { 37 | var components = URLComponents(string: base)! 38 | components.path = components.path.appending(path) 39 | if !queryItems.isEmpty { 40 | components.queryItems = queryItems 41 | } 42 | return components 43 | } 44 | 45 | /* 46 | curl -X POST https://api.anthropic.com/v1/messages \ 47 | --header "x-api-key: $ANTHROPIC_API_KEY" \ 48 | --header "anthropic-version: 2023-06-01" \ 49 | --header "anthropic-beta: messages-2023-12-15" \ 50 | --header "content-type: application/json" \ 51 | --data \ 52 | '{ 53 | "model": "claude-2.1", 54 | "max_tokens": 1024, 55 | "messages": [ 56 | {"role": "user", "content": "Hello, Claude"} 57 | ] 58 | }' 59 | */ 60 | func request( 61 | apiKey: String, 62 | version: String, 63 | method: HTTPMethod, 64 | params: Encodable? = nil, 65 | betaHeaders: [String]? = nil, 66 | queryItems: [URLQueryItem] = []) 67 | throws -> URLRequest 68 | { 69 | var request = URLRequest(url: urlComponents(queryItems: queryItems).url!) 70 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 71 | request.addValue("\(apiKey)", forHTTPHeaderField: "x-api-key") 72 | request.addValue("\(version)", forHTTPHeaderField: "anthropic-version") 73 | if let betaHeaders { 74 | request.addValue("\(betaHeaders.joined(separator: ","))", forHTTPHeaderField: "anthropic-beta") 75 | } 76 | request.httpMethod = method.rawValue 77 | if let params { 78 | let encoder = JSONEncoder() 79 | encoder.keyEncodingStrategy = .convertToSnakeCase 80 | request.httpBody = try encoder.encode(params) 81 | } 82 | return request 83 | } 84 | 85 | /// Creates a multipart/form-data request for uploading skill files. 86 | /// 87 | /// This method is used for Skills API endpoints that require file uploads. 88 | func multipartRequest( 89 | apiKey: String, 90 | version: String, 91 | method: HTTPMethod, 92 | displayTitle: String?, 93 | files: [SkillFile], 94 | betaHeaders: [String]? = nil, 95 | queryItems: [URLQueryItem] = []) 96 | throws -> URLRequest 97 | { 98 | let boundary = "Boundary-\(UUID().uuidString)" 99 | var request = URLRequest(url: urlComponents(queryItems: queryItems).url!) 100 | request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") 101 | request.addValue("\(apiKey)", forHTTPHeaderField: "x-api-key") 102 | request.addValue("\(version)", forHTTPHeaderField: "anthropic-version") 103 | if let betaHeaders { 104 | request.addValue("\(betaHeaders.joined(separator: ","))", forHTTPHeaderField: "anthropic-beta") 105 | } 106 | request.httpMethod = method.rawValue 107 | 108 | // Build multipart body 109 | var body = Data() 110 | 111 | // Add display_title if provided 112 | if let displayTitle = displayTitle { 113 | body.append("--\(boundary)\r\n".data(using: .utf8)!) 114 | body.append("Content-Disposition: form-data; name=\"display_title\"\r\n\r\n".data(using: .utf8)!) 115 | body.append("\(displayTitle)\r\n".data(using: .utf8)!) 116 | } 117 | 118 | // Add files 119 | for file in files { 120 | body.append("--\(boundary)\r\n".data(using: .utf8)!) 121 | 122 | // Format: files[]=@path/to/file;filename=skill_name/SKILL.md 123 | let contentDisposition = "Content-Disposition: form-data; name=\"files[]\"; filename=\"\(file.filename)\"\r\n" 124 | body.append(contentDisposition.data(using: .utf8)!) 125 | 126 | if let mimeType = file.mimeType { 127 | body.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!) 128 | } else { 129 | body.append("\r\n".data(using: .utf8)!) 130 | } 131 | 132 | body.append(file.data) 133 | body.append("\r\n".data(using: .utf8)!) 134 | } 135 | 136 | // Close boundary 137 | body.append("--\(boundary)--\r\n".data(using: .utf8)!) 138 | 139 | request.httpBody = body 140 | return request 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Sources/Anthropic/Private/Networking/AsyncHTTPClientAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncHTTPClientAdapter.swift 3 | // SwiftAnthropic 4 | // 5 | // Created by Joe Fabisevich on 5/18/25. 6 | // 7 | 8 | #if os(Linux) 9 | import AsyncHTTPClient 10 | import Foundation 11 | import NIOCore 12 | import NIOFoundationCompat 13 | import NIOHTTP1 14 | 15 | /// Adapter that implements HTTPClient protocol using AsyncHTTPClient 16 | public class AsyncHTTPClientAdapter: HTTPClient { 17 | /// Initializes a new AsyncHTTPClientAdapter with the provided AsyncHTTPClient 18 | /// - Parameter client: The AsyncHTTPClient instance to use 19 | public init(client: AsyncHTTPClient.HTTPClient) { 20 | self.client = client 21 | } 22 | 23 | deinit { 24 | shutdown() 25 | } 26 | 27 | /// Creates a new AsyncHTTPClientAdapter with a default configuration 28 | /// - Returns: A new AsyncHTTPClientAdapter instance 29 | public static func createDefault() -> AsyncHTTPClientAdapter { 30 | let httpClient = AsyncHTTPClient.HTTPClient( 31 | eventLoopGroupProvider: .singleton, 32 | configuration: AsyncHTTPClient.HTTPClient.Configuration( 33 | certificateVerification: .fullVerification, 34 | timeout: .init( 35 | connect: .seconds(30), 36 | read: .seconds(30)), 37 | backgroundActivityLogger: nil)) 38 | return AsyncHTTPClientAdapter(client: httpClient) 39 | } 40 | 41 | /// Fetches data for a given HTTP request 42 | /// - Parameter request: The HTTP request to perform 43 | /// - Returns: A tuple containing the data and HTTP response 44 | public func data(for request: HTTPRequest) async throws -> (Data, HTTPResponse) { 45 | let asyncHTTPClientRequest = try createAsyncHTTPClientRequest(from: request) 46 | 47 | let response = try await client.execute(asyncHTTPClientRequest, deadline: .now() + .seconds(60)) 48 | let body = try await response.body.collect(upTo: 100 * 1024 * 1024) // 100 MB max 49 | 50 | let data = Data(buffer: body) 51 | let httpResponse = HTTPResponse( 52 | statusCode: Int(response.status.code), 53 | headers: convertHeaders(response.headers)) 54 | 55 | return (data, httpResponse) 56 | } 57 | 58 | /// Fetches a byte stream for a given HTTP request 59 | /// - Parameter request: The HTTP request to perform 60 | /// - Returns: A tuple containing the byte stream and HTTP response 61 | public func bytes(for request: HTTPRequest) async throws -> (HTTPByteStream, HTTPResponse) { 62 | let asyncHTTPClientRequest = try createAsyncHTTPClientRequest(from: request) 63 | 64 | let response = try await client.execute(asyncHTTPClientRequest, deadline: .now() + .seconds(60)) 65 | let httpResponse = HTTPResponse( 66 | statusCode: Int(response.status.code), 67 | headers: convertHeaders(response.headers)) 68 | 69 | let stream = AsyncThrowingStream { continuation in 70 | Task { 71 | do { 72 | for try await byteBuffer in response.body { 73 | if let string = byteBuffer.getString(at: 0, length: byteBuffer.readableBytes) { 74 | let lines = string.split(separator: "\n", omittingEmptySubsequences: false) 75 | for line in lines { 76 | continuation.yield(String(line)) 77 | } 78 | } 79 | } 80 | continuation.finish() 81 | } catch { 82 | continuation.finish(throwing: error) 83 | } 84 | } 85 | } 86 | 87 | return (.lines(stream), httpResponse) 88 | } 89 | 90 | /// Properly shutdown the HTTP client 91 | public func shutdown() { 92 | try? client.shutdown().wait() 93 | } 94 | 95 | /// The underlying AsyncHTTPClient instance 96 | private let client: AsyncHTTPClient.HTTPClient 97 | 98 | /// Converts our HTTPRequest to AsyncHTTPClient's Request 99 | /// - Parameter request: Our HTTPRequest 100 | /// - Returns: AsyncHTTPClient Request 101 | private func createAsyncHTTPClientRequest(from request: HTTPRequest) throws -> HTTPClientRequest { 102 | var asyncHTTPClientRequest = HTTPClientRequest(url: request.url.absoluteString) 103 | asyncHTTPClientRequest.method = NIOHTTP1.HTTPMethod(rawValue: request.method.rawValue) 104 | 105 | // Add headers 106 | for (key, value) in request.headers { 107 | asyncHTTPClientRequest.headers.add(name: key, value: value) 108 | } 109 | 110 | // Add body if present 111 | if let body = request.body { 112 | asyncHTTPClientRequest.body = .bytes(body) 113 | } 114 | 115 | return asyncHTTPClientRequest 116 | } 117 | 118 | /// Converts NIOHTTP1 headers to a dictionary 119 | /// - Parameter headers: NIOHTTP1 HTTPHeaders 120 | /// - Returns: Dictionary of header name-value pairs 121 | private func convertHeaders(_ headers: HTTPHeaders) -> [String: String] { 122 | var result = [String: String]() 123 | for header in headers { 124 | result[header.name] = header.value 125 | } 126 | return result 127 | } 128 | 129 | } 130 | #endif -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExample/Thinking/ThinkingModeMessageDemoObservable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThinkingModeMessageDemoObservable.swift 3 | // SwiftAnthropic 4 | // 5 | // Created by James Rochabrun on 2/24/25. 6 | // 7 | 8 | import Foundation 9 | import SwiftAnthropic 10 | import SwiftUI 11 | 12 | @MainActor 13 | @Observable class ThinkingModeMessageDemoObservable { 14 | 15 | let service: AnthropicService 16 | var message: String = "" 17 | var thinkingContentMessage = "" 18 | var errorMessage: String = "" 19 | var isLoading = false 20 | var inputTokensCount: String? 21 | 22 | // State for managing conversation 23 | private var messages: [MessageParameter.Message] = [] 24 | 25 | // Handler for processing thinking content 26 | private var streamHandler = StreamHandler() 27 | 28 | init(service: AnthropicService) { 29 | self.service = service 30 | } 31 | 32 | // Send a message to Claude with thinking enabled 33 | func sendMessage(prompt: String, budgetTokens: Int = 16000) async throws { 34 | guard !prompt.isEmpty else { 35 | errorMessage = "Please enter a prompt" 36 | return 37 | } 38 | 39 | // Reset state for new response 40 | message = "" 41 | thinkingContentMessage = "" 42 | errorMessage = "" 43 | streamHandler.reset() // Clear previous stream data 44 | 45 | // Add user message to conversation 46 | let userMessage = MessageParameter.Message( 47 | role: .user, 48 | content: .text(prompt) 49 | ) 50 | messages.append(userMessage) 51 | 52 | // Create parameters with thinking enabled 53 | let parameters = MessageParameter( 54 | model: .claude37Sonnet, 55 | messages: messages, 56 | maxTokens: 20000, 57 | stream: true, 58 | thinking: .init(budgetTokens: budgetTokens) 59 | ) 60 | 61 | // Count tokens (optional) 62 | let tokenCountParams = MessageTokenCountParameter( 63 | model: .claude37Sonnet, 64 | messages: messages 65 | ) 66 | 67 | do { 68 | // Get token count 69 | let tokenCount = try await service.countTokens(parameter: tokenCountParams) 70 | inputTokensCount = "\(tokenCount.inputTokens)" 71 | 72 | // Stream the response 73 | isLoading = true 74 | let stream = try await service.streamMessage(parameters) 75 | 76 | // Process stream events 77 | for try await result in stream { 78 | // Use the ThinkingStreamHandler to process events 79 | streamHandler.handleStreamEvent(result) 80 | 81 | // Update UI elements based on event type 82 | updateUIFromStreamEvent(result) 83 | } 84 | 85 | // Once streaming is complete, store assistant's response in conversation history 86 | let finalMessage = streamHandler.textResponse 87 | if !finalMessage.isEmpty { 88 | // Get thinking blocks from the handler 89 | let thinkingBlocks = streamHandler.getThinkingBlocksForAPI() 90 | 91 | // Create content objects: thinking blocks + text 92 | var contentObjects = thinkingBlocks 93 | contentObjects.append(.text(finalMessage)) 94 | 95 | // Create assistant message with both thinking blocks and text 96 | let assistantMessage = MessageParameter.Message( 97 | role: .assistant, 98 | content: .list(contentObjects) 99 | ) 100 | 101 | // Add to conversation history 102 | messages.append(assistantMessage) 103 | message = finalMessage // Update UI 104 | } 105 | 106 | isLoading = false 107 | } catch { 108 | isLoading = false 109 | errorMessage = "Error: \(error.localizedDescription)" 110 | } 111 | } 112 | 113 | // Just update UI elements based on stream events, no need to track state 114 | private func updateUIFromStreamEvent(_ event: MessageStreamResponse) { 115 | // Update UI elements based on deltas 116 | if let delta = event.delta { 117 | switch delta.type { 118 | case "thinking_delta": 119 | if let thinking = delta.thinking { 120 | // Update the thinking content shown in UI 121 | thinkingContentMessage += thinking 122 | } 123 | case "text_delta": 124 | if let text = delta.text { 125 | // Update the message shown in UI 126 | message += text 127 | } 128 | default: 129 | break 130 | } 131 | } 132 | } 133 | 134 | func clearConversation() { 135 | message = "" 136 | thinkingContentMessage = "" 137 | errorMessage = "" 138 | messages.removeAll() 139 | inputTokensCount = nil 140 | streamHandler.reset() 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExample/Skills/SkillsDemoObservable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkillsDemoObservable.swift 3 | // SwiftAnthropicExample 4 | // 5 | // Created by James Rochabrun on 10/25/25. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftAnthropic 10 | 11 | @MainActor 12 | @Observable class SkillsDemoObservable { 13 | 14 | let service: AnthropicService 15 | 16 | var message: String = "" 17 | var errorMessage: String = "" 18 | var isLoading = false 19 | var availableSkills: [SkillResponse] = [] 20 | var containerId: String? 21 | 22 | init(service: AnthropicService) { 23 | self.service = service 24 | } 25 | 26 | /// Lists available skills 27 | func listSkills() async { 28 | isLoading = true 29 | errorMessage = "" 30 | 31 | do { 32 | let response = try await service.listSkills(parameter: nil) 33 | availableSkills = response.data 34 | message = "Found \(response.data.count) skill(s)\n\n" 35 | for skill in response.data { 36 | message += "📦 \(skill.displayTitle ?? "No title")\n" 37 | message += " ID: \(skill.id)\n" 38 | message += " Source: \(skill.source)\n" 39 | message += " Version: \(skill.latestVersion ?? "none")\n\n" 40 | } 41 | } catch { 42 | errorMessage = error.localizedDescription 43 | } 44 | 45 | isLoading = false 46 | } 47 | 48 | /// Sends a message using a skill (e.g., XLSX skill) 49 | func createMessageWithSkill() async { 50 | isLoading = true 51 | errorMessage = "" 52 | message = "" 53 | 54 | do { 55 | // Example: Use the xlsx skill to create a spreadsheet 56 | let parameter = MessageParameter( 57 | model: .claude37Sonnet, 58 | messages: [ 59 | .init( 60 | role: .user, 61 | content: .text("Create a simple budget spreadsheet with categories: Housing, Food, Transportation, and Entertainment. Add sample monthly amounts.") 62 | ) 63 | ], 64 | maxTokens: 4096, 65 | tools: [ 66 | .hosted(type: "code_execution_20250825", name: "code_execution") 67 | ], 68 | container: .init( 69 | id: containerId, // Reuse container if available 70 | skills: [ 71 | .init(type: .anthropic, skillId: "xlsx", version: "latest") 72 | ] 73 | ) 74 | ) 75 | 76 | let response = try await service.createMessage(parameter) 77 | 78 | // Save container ID for reuse 79 | if let newContainerId = response.container?.id { 80 | containerId = newContainerId 81 | message += "📦 Container ID: \(newContainerId)\n\n" 82 | } 83 | 84 | // Display response 85 | for content in response.content { 86 | switch content { 87 | case .text(let text, _): 88 | message += text + "\n" 89 | case .toolUse(let toolUse): 90 | message += "\n🔧 Tool: \(toolUse.name)\n" 91 | message += "Input: \(toolUse.input)\n" 92 | default: 93 | message += "Other content type\n" 94 | } 95 | } 96 | 97 | if let stopReason = response.stopReason { 98 | message += "\n⏹️ Stop reason: \(stopReason)" 99 | } 100 | 101 | } catch { 102 | errorMessage = error.localizedDescription 103 | } 104 | 105 | isLoading = false 106 | } 107 | 108 | /// Streams a message using a skill 109 | func streamMessageWithSkill() async { 110 | isLoading = true 111 | errorMessage = "" 112 | message = "" 113 | 114 | do { 115 | let parameter = MessageParameter( 116 | model: .claude37Sonnet, 117 | messages: [ 118 | .init( 119 | role: .user, 120 | content: .text("Analyze this data and create a chart: Q1: $10k, Q2: $15k, Q3: $12k, Q4: $18k") 121 | ) 122 | ], 123 | maxTokens: 4096, 124 | stream: true, 125 | tools: [ 126 | .hosted(type: "code_execution_20250825", name: "code_execution") 127 | ], 128 | container: .init( 129 | id: containerId, 130 | skills: [ 131 | .init(type: .anthropic, skillId: "xlsx", version: "latest") 132 | ] 133 | ) 134 | ) 135 | 136 | let stream = try await service.streamMessage(parameter) 137 | 138 | for try await chunk in stream { 139 | if let delta = chunk.delta { 140 | if let text = delta.text { 141 | message += text 142 | } 143 | } 144 | 145 | if let newContainerId = chunk.message?.container?.id { 146 | containerId = newContainerId 147 | } 148 | } 149 | 150 | } catch { 151 | errorMessage = error.localizedDescription 152 | } 153 | 154 | isLoading = false 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExample/Messages/MessageDemoObservable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageDemoObservable.swift 3 | // SwiftAnthropicExample 4 | // 5 | // Created by James Rochabrun on 2/24/24. 6 | // 7 | 8 | import Foundation 9 | import SwiftAnthropic 10 | import SwiftUI 11 | 12 | @MainActor 13 | @Observable class MessageDemoObservable { 14 | 15 | let service: AnthropicService 16 | var message: String = "" 17 | var errorMessage: String = "" 18 | var isLoading = false 19 | var selectedPDF: Data? = nil 20 | var inputTokensCount: String? 21 | 22 | init(service: AnthropicService) { 23 | self.service = service 24 | } 25 | 26 | func createMessage( 27 | parameters: MessageParameter) async throws 28 | { 29 | task = Task { 30 | do { 31 | isLoading = true 32 | let message = try await service.createMessage(parameters) 33 | isLoading = false 34 | switch message.content.first { 35 | case .text(let text, let citations): 36 | // We need to concatenate the text response. 37 | self.message = text 38 | if let citations { 39 | dump(citations) 40 | } 41 | default: 42 | /// Function call not implemented on this demo 43 | break 44 | } 45 | } catch { 46 | self.errorMessage = "\(error)" 47 | } 48 | } 49 | } 50 | 51 | func streamMessage( 52 | parameters: MessageParameter) async throws 53 | { 54 | task = Task { 55 | do { 56 | var citationCitedText = "" 57 | isLoading = true 58 | let stream = try await service.streamMessage(parameters) 59 | isLoading = false 60 | for try await result in stream { 61 | let content = result.delta?.text ?? "" 62 | self.message += content 63 | switch result.delta?.citation { 64 | case .charLocation(let charLocation): 65 | citationCitedText += charLocation.citedText ?? "" 66 | case .contentBlockLocation(let blockLocation): 67 | citationCitedText += blockLocation.citedText ?? "" 68 | case .pageLocation(let pageLocation): 69 | citationCitedText += pageLocation.citedText ?? "" 70 | default: break 71 | } 72 | } 73 | if !citationCitedText.isEmpty { 74 | debugPrint("Citation Text: \n \(citationCitedText)") 75 | } 76 | } catch { 77 | self.errorMessage = "\(error)" 78 | } 79 | } 80 | } 81 | 82 | func countTokens(parameters: MessageTokenCountParameter) async throws { 83 | let inputTokens = try await service.countTokens(parameter: parameters) 84 | inputTokensCount = "\(inputTokens.inputTokens)" 85 | } 86 | 87 | func analyzePDF(prompt: String, selectedSegment: MessageDemoView.ChatConfig) async throws { 88 | guard let pdfData = selectedPDF else { 89 | errorMessage = "No PDF selected" 90 | return 91 | } 92 | 93 | // Convert PDF to base64 94 | let base64PDF = pdfData.base64EncodedString() 95 | 96 | do { 97 | // Create document source with citations enabled 98 | let documentSource = try MessageParameter.Message.Content.DocumentSource.pdf(base64Data: base64PDF, citations: .init(enabled: true)) 99 | 100 | // Create message with document and prompt 101 | let message = MessageParameter.Message( 102 | role: .user, 103 | content: .list([ 104 | .document(documentSource), 105 | .text(prompt.isEmpty ? "Please analyze this document and provide a summary" : prompt) 106 | ]) 107 | ) 108 | 109 | // Create parameters 110 | let parameters = MessageParameter( 111 | model: .claude35Sonnet, 112 | messages: [message], 113 | maxTokens: 1024 114 | ) 115 | 116 | // Send request based on selected mode 117 | switch selectedSegment { 118 | case .message: 119 | try await createMessage(parameters: parameters) 120 | case .messageStream: 121 | try await streamMessage(parameters: parameters) 122 | } 123 | 124 | } catch MessageParameter.Message.Content.DocumentSource.DocumentError.exceededSizeLimit { 125 | errorMessage = "PDF exceeds size limit (32MB)" 126 | } catch MessageParameter.Message.Content.DocumentSource.DocumentError.invalidBase64Data { 127 | errorMessage = "Invalid PDF data" 128 | } catch { 129 | errorMessage = "Error analyzing PDF: \(error.localizedDescription)" 130 | } 131 | } 132 | 133 | func cancelStream() { 134 | task?.cancel() 135 | } 136 | 137 | func clearMessage() { 138 | message = "" 139 | selectedPDF = nil 140 | errorMessage = "" 141 | } 142 | 143 | // MARK: Private 144 | private var task: Task? = nil 145 | // Track the current active content block 146 | private var currentThinking = "" 147 | private var currentBlockType: String? 148 | private var currentBlockIndex: Int? 149 | } 150 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExample/Thinking/ThinkingModeMessageDemoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThinkingModeMessageDemoView.swift 3 | // SwiftAnthropic 4 | // 5 | // Created by James Rochabrun on 2/24/25. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftAnthropic 10 | 11 | @MainActor 12 | struct ThinkingModeMessageDemoView: View { 13 | 14 | let observable: ThinkingModeMessageDemoObservable 15 | @State private var prompt: String = "" 16 | @State private var thinkingBudget: Double = 16.0 17 | @State private var showThinking: Bool = true 18 | 19 | var body: some View { 20 | VStack { 21 | // Header with token count 22 | HStack { 23 | Text("Claude 3.7 with Extended Thinking") 24 | .font(.headline) 25 | Spacer() 26 | if let inputTokensCount = observable.inputTokensCount { 27 | Text("Tokens: \(inputTokensCount)") 28 | .font(.caption) 29 | .foregroundColor(.secondary) 30 | } 31 | } 32 | .padding() 33 | 34 | // Thinking budget slider 35 | VStack(alignment: .leading) { 36 | Text("Thinking Budget: \(Int(thinkingBudget * 1000)) tokens") 37 | .font(.caption) 38 | Slider(value: $thinkingBudget, in: 1...32) 39 | } 40 | .padding(.horizontal) 41 | 42 | // Show/hide thinking toggle 43 | Toggle("Show Thinking Process", isOn: $showThinking) 44 | .padding(.horizontal) 45 | 46 | // Error message 47 | if !observable.errorMessage.isEmpty { 48 | Text(observable.errorMessage) 49 | .foregroundColor(.red) 50 | .padding() 51 | } 52 | 53 | // Main content area (scrollable) 54 | ScrollView { 55 | VStack(alignment: .leading, spacing: 16) { 56 | // Thinking content (only shown if toggle is on) 57 | if showThinking && !observable.thinkingContentMessage.isEmpty { 58 | VStack(alignment: .leading) { 59 | Text("Claude's Thinking:") 60 | .font(.subheadline) 61 | .fontWeight(.bold) 62 | .foregroundColor(.blue) 63 | 64 | Text(observable.thinkingContentMessage) 65 | .foregroundColor(.blue.opacity(0.8)) 66 | .padding() 67 | .background(Color.blue.opacity(0.1)) 68 | .cornerRadius(8) 69 | } 70 | .padding(.horizontal) 71 | } 72 | 73 | // Model's response 74 | if !observable.message.isEmpty { 75 | VStack(alignment: .leading) { 76 | Text("Claude's Response:") 77 | .font(.subheadline) 78 | .fontWeight(.bold) 79 | 80 | Text(observable.message) 81 | .padding() 82 | .background(Color.gray.opacity(0.1)) 83 | .cornerRadius(8) 84 | } 85 | .padding(.horizontal) 86 | } 87 | } 88 | .padding(.bottom, 100) // Extra padding for input area 89 | } 90 | 91 | Spacer() 92 | 93 | // Input area (fixed at bottom) 94 | VStack { 95 | HStack { 96 | Button("Clear Conversation") { 97 | observable.clearConversation() 98 | prompt = "" 99 | } 100 | .buttonStyle(.bordered) 101 | 102 | Spacer() 103 | } 104 | .padding(.horizontal) 105 | 106 | HStack { 107 | TextField("Enter your message...", text: $prompt, axis: .vertical) 108 | .textFieldStyle(.roundedBorder) 109 | .lineLimit(1...5) 110 | 111 | Button { 112 | Task { 113 | try await observable.sendMessage( 114 | prompt: prompt, 115 | budgetTokens: Int(thinkingBudget * 1000) 116 | ) 117 | prompt = "" 118 | } 119 | } label: { 120 | Image(systemName: "paperplane.fill") 121 | .foregroundColor(.blue) 122 | } 123 | .disabled(prompt.isEmpty || observable.isLoading) 124 | } 125 | .padding() 126 | } 127 | .background(Color(UIColor.systemBackground)) 128 | } 129 | .overlay( 130 | Group { 131 | if observable.isLoading { 132 | VStack { 133 | ProgressView() 134 | .scaleEffect(1.5) 135 | Text("Claude is thinking...") 136 | .padding(.top) 137 | } 138 | .padding() 139 | .background(Color(UIColor.systemBackground).opacity(0.8)) 140 | .cornerRadius(10) 141 | .shadow(radius: 10) 142 | } 143 | } 144 | ) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Sources/Anthropic/Public/Parameters/Message/JSONSchema.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONSchema.swift 3 | // SwiftAnthropic 4 | // 5 | // Created by James Rochabrun on 3/16/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct JSONSchema: Codable, Equatable { 11 | 12 | public let type: JSONType 13 | public let properties: [String: Property]? 14 | public let required: [String]? 15 | public let pattern: String? 16 | public let const: String? 17 | public let enumValues: [String]? 18 | public let multipleOf: Int? 19 | public let minimum: Int? 20 | public let maximum: Int? 21 | 22 | private enum CodingKeys: String, CodingKey { 23 | case type, properties, required, pattern, const 24 | case enumValues = "enum" 25 | case multipleOf, minimum, maximum 26 | } 27 | 28 | public struct Property: Codable, Equatable { 29 | 30 | public let type: JSONType 31 | public let description: String? 32 | public let format: String? 33 | public let items: Items? 34 | public let required: [String]? 35 | public let pattern: String? 36 | public let const: String? 37 | public let enumValues: [String]? 38 | public let multipleOf: Int? 39 | public let minimum: Double? 40 | public let maximum: Double? 41 | public let minItems: Int? 42 | public let maxItems: Int? 43 | public let uniqueItems: Bool? 44 | 45 | private enum CodingKeys: String, CodingKey { 46 | case type, description, format, items, required, pattern, const 47 | case enumValues = "enum" 48 | case multipleOf, minimum, maximum 49 | case minItems, maxItems, uniqueItems 50 | } 51 | 52 | public init( 53 | type: JSONType, 54 | description: String? = nil, 55 | format: String? = nil, 56 | items: Items? = nil, 57 | required: [String]? = nil, 58 | pattern: String? = nil, 59 | const: String? = nil, 60 | enumValues: [String]? = nil, 61 | multipleOf: Int? = nil, 62 | minimum: Double? = nil, 63 | maximum: Double? = nil, 64 | minItems: Int? = nil, 65 | maxItems: Int? = nil, 66 | uniqueItems: Bool? = nil) 67 | { 68 | self.type = type 69 | self.description = description 70 | self.format = format 71 | self.items = items 72 | self.required = required 73 | self.pattern = pattern 74 | self.const = const 75 | self.enumValues = enumValues 76 | self.multipleOf = multipleOf 77 | self.minimum = minimum 78 | self.maximum = maximum 79 | self.minItems = minItems 80 | self.maxItems = maxItems 81 | self.uniqueItems = uniqueItems 82 | } 83 | } 84 | 85 | public enum JSONType: String, Codable { 86 | case integer = "integer" 87 | case string = "string" 88 | case boolean = "boolean" 89 | case array = "array" 90 | case object = "object" 91 | case number = "number" 92 | case `null` = "null" 93 | } 94 | 95 | public struct Items: Codable, Equatable { 96 | 97 | public let type: JSONType 98 | public let properties: [String: Property]? 99 | public let pattern: String? 100 | public let const: String? 101 | public let enumValues: [String]? 102 | public let multipleOf: Int? 103 | public let minimum: Double? 104 | public let maximum: Double? 105 | public let minItems: Int? 106 | public let maxItems: Int? 107 | public let uniqueItems: Bool? 108 | 109 | private enum CodingKeys: String, CodingKey { 110 | case type, properties, pattern, const 111 | case enumValues = "enum" 112 | case multipleOf, minimum, maximum, minItems, maxItems, uniqueItems 113 | } 114 | 115 | public init( 116 | type: JSONType, 117 | properties: [String : Property]? = nil, 118 | pattern: String? = nil, 119 | const: String? = nil, 120 | enumValues: [String]? = nil, 121 | multipleOf: Int? = nil, 122 | minimum: Double? = nil, 123 | maximum: Double? = nil, 124 | minItems: Int? = nil, 125 | maxItems: Int? = nil, 126 | uniqueItems: Bool? = nil) 127 | { 128 | self.type = type 129 | self.properties = properties 130 | self.pattern = pattern 131 | self.const = const 132 | self.enumValues = enumValues 133 | self.multipleOf = multipleOf 134 | self.minimum = minimum 135 | self.maximum = maximum 136 | self.minItems = minItems 137 | self.maxItems = maxItems 138 | self.uniqueItems = uniqueItems 139 | } 140 | } 141 | 142 | public init( 143 | type: JSONType, 144 | properties: [String : Property]? = nil, 145 | required: [String]? = nil, 146 | pattern: String? = nil, 147 | const: String? = nil, 148 | enumValues: [String]? = nil, 149 | multipleOf: Int? = nil, 150 | minimum: Int? = nil, 151 | maximum: Int? = nil) 152 | { 153 | self.type = type 154 | self.properties = properties 155 | self.required = required 156 | self.pattern = pattern 157 | self.const = const 158 | self.enumValues = enumValues 159 | self.multipleOf = multipleOf 160 | self.minimum = minimum 161 | self.maximum = maximum 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /Sources/Anthropic/Public/ResponseModels/Message/MessageStreamResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageStreamResponse.swift 3 | // 4 | // 5 | // Created by James Rochabrun on 1/28/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// [Message Stream Response](https://docs.anthropic.com/claude/reference/messages-streaming). 11 | /// 12 | /// Each server-sent event includes a named event type and associated JSON data. Each event will use an SSE event name (e.g. event: message_stop), and include the matching event type in its data. 13 | /// 14 | /// Each stream uses the following event flow: 15 | /// 16 | /// message_start: contains a Message object with empty content. 17 | /// A series of content blocks, each of which have a content_block_start, one or more content_block_delta events, and a content_block_stop event. Each content block will have an index that corresponds to its index in the final Message content array. 18 | /// One or more message_delta events, indicating top-level changes to the final Message object. 19 | /// A final message_stop event. 20 | /// 21 | /// This structured sequence facilitates the orderly reception and processing of message components and overall changes. 22 | public struct MessageStreamResponse: Decodable { 23 | 24 | public let type: String 25 | 26 | public let index: Int? 27 | 28 | /// available in "content_block_start" event 29 | public let contentBlock: ContentBlock? 30 | 31 | /// available in "message_start" event 32 | public let message: MessageResponse? 33 | 34 | /// Available in "content_block_delta", "message_delta" events. 35 | public let delta: Delta? 36 | 37 | /// Available in "message_delta" events. 38 | public let usage: MessageResponse.Usage? 39 | 40 | public var streamEvent: StreamEvent? { 41 | StreamEvent(rawValue: type) 42 | } 43 | 44 | public let error: Error? 45 | 46 | /// Web search tool result 47 | public let toolUseId: String? 48 | 49 | public let content: [WebSearchResult]? 50 | 51 | public struct Delta: Decodable { 52 | public let type: String? 53 | 54 | /// type = text 55 | public let text: String? 56 | 57 | /// type = thinking_delta 58 | public let thinking: String? 59 | 60 | /// type = signature_delta 61 | public let signature: String? 62 | 63 | /// type = tool_use 64 | public let partialJson: String? 65 | 66 | // type = citations_delta 67 | public let citation: MessageResponse.Citation? 68 | 69 | public let stopReason: String? 70 | 71 | public let stopSequence: String? 72 | } 73 | 74 | public struct ContentBlock: Decodable { 75 | 76 | // Can be of type `text`, `tool_use`, `thinking`, or `redacted_thinking` 77 | public let type: String 78 | 79 | /// `text` type 80 | public let text: String? 81 | 82 | /// `thinking` type 83 | public let thinking: String? 84 | 85 | /// `redacted_thinking` type 86 | public let data: String? 87 | 88 | // Citations for text type 89 | public let citations: [MessageResponse.Citation]? 90 | 91 | /// `tool_use` and `server_tool_use` type 92 | public let input: [String: MessageResponse.Content.DynamicContent]? 93 | 94 | public let name: String? 95 | 96 | public let id: String? 97 | 98 | public var toolUse: MessageResponse.Content.ToolUse? { 99 | guard let name, let id else { return nil } 100 | return .init(id: id, name: name, input: input ?? [:]) 101 | } 102 | } 103 | 104 | public struct Error: Decodable { 105 | 106 | /// The error type, for example "overloaded_error" 107 | public let type: String 108 | 109 | /// The error message, for example "Overloaded" 110 | public let message: String 111 | } 112 | 113 | /// https://docs.anthropic.com/en/api/messages-streaming#event-types 114 | public enum StreamEvent: String { 115 | 116 | case contentBlockStart = "content_block_start" 117 | case contentBlockDelta = "content_block_delta" 118 | case contentBlockStop = "content_block_stop" 119 | case messageStart = "message_start" 120 | case messageDelta = "message_delta" 121 | case messageStop = "message_stop" 122 | } 123 | } 124 | 125 | // MARK: - Web Search Results 126 | 127 | public struct WebSearchResult: Decodable { 128 | public let type: String? 129 | public let url: String? 130 | public let title: String? 131 | public let encryptedContent: String? 132 | public let pageAge: String? 133 | public let errorCode: String? 134 | } 135 | 136 | extension MessageStreamResponse { 137 | 138 | /// Helper to check if the delta contains thinking content 139 | public var isThinkingDelta: Bool { 140 | return delta?.type == "thinking_delta" 141 | } 142 | 143 | /// Helper to check if the delta contains a signature update 144 | public var isSignatureDelta: Bool { 145 | return delta?.type == "signature_delta" 146 | } 147 | 148 | /// Helper to check if the content block is a thinking block 149 | public var isThinkingBlock: Bool { 150 | return contentBlock?.type == "thinking" 151 | } 152 | 153 | /// Helper to check if the content block is a redacted thinking block 154 | public var isRedactedThinkingBlock: Bool { 155 | return contentBlock?.type == "redacted_thinking" 156 | } 157 | 158 | /// Helper to check if this is a server tool use (web search) 159 | public var isServerToolUse: Bool { 160 | return contentBlock?.type == "server_tool_use" 161 | } 162 | 163 | /// Helper to check if this is a web search tool result 164 | public var isWebSearchToolResult: Bool { 165 | return type == "web_search_tool_result" 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "async-http-client", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/swift-server/async-http-client.git", 7 | "state" : { 8 | "revision" : "60235983163d040f343a489f7e2e77c1918a8bd9", 9 | "version" : "1.26.1" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-algorithms", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-algorithms.git", 16 | "state" : { 17 | "revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023", 18 | "version" : "1.2.1" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-asn1", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-asn1.git", 25 | "state" : { 26 | "revision" : "f70225981241859eb4aa1a18a75531d26637c8cc", 27 | "version" : "1.4.0" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-async-algorithms", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/apple/swift-async-algorithms.git", 34 | "state" : { 35 | "revision" : "042e1c4d9d19748c9c228f8d4ebc97bb1e339b0b", 36 | "version" : "1.0.4" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-atomics", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-atomics.git", 43 | "state" : { 44 | "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", 45 | "version" : "1.3.0" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-certificates", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/apple/swift-certificates.git", 52 | "state" : { 53 | "revision" : "870f4d5fe5fcfedc13f25d70e103150511746404", 54 | "version" : "1.11.0" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-collections", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/apple/swift-collections.git", 61 | "state" : { 62 | "revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0", 63 | "version" : "1.2.0" 64 | } 65 | }, 66 | { 67 | "identity" : "swift-crypto", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/apple/swift-crypto.git", 70 | "state" : { 71 | "revision" : "e8d6eba1fef23ae5b359c46b03f7d94be2f41fed", 72 | "version" : "3.12.3" 73 | } 74 | }, 75 | { 76 | "identity" : "swift-http-structured-headers", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/apple/swift-http-structured-headers.git", 79 | "state" : { 80 | "revision" : "db6eea3692638a65e2124990155cd220c2915903", 81 | "version" : "1.3.0" 82 | } 83 | }, 84 | { 85 | "identity" : "swift-http-types", 86 | "kind" : "remoteSourceControl", 87 | "location" : "https://github.com/apple/swift-http-types.git", 88 | "state" : { 89 | "revision" : "a0a57e949a8903563aba4615869310c0ebf14c03", 90 | "version" : "1.4.0" 91 | } 92 | }, 93 | { 94 | "identity" : "swift-log", 95 | "kind" : "remoteSourceControl", 96 | "location" : "https://github.com/apple/swift-log.git", 97 | "state" : { 98 | "revision" : "3d8596ed08bd13520157f0355e35caed215ffbfa", 99 | "version" : "1.6.3" 100 | } 101 | }, 102 | { 103 | "identity" : "swift-nio", 104 | "kind" : "remoteSourceControl", 105 | "location" : "https://github.com/apple/swift-nio.git", 106 | "state" : { 107 | "revision" : "ad6b5f17270a7008f60d35ec5378e6144a575162", 108 | "version" : "2.84.0" 109 | } 110 | }, 111 | { 112 | "identity" : "swift-nio-extras", 113 | "kind" : "remoteSourceControl", 114 | "location" : "https://github.com/apple/swift-nio-extras.git", 115 | "state" : { 116 | "revision" : "145db1962f4f33a4ea07a32e751d5217602eea29", 117 | "version" : "1.28.0" 118 | } 119 | }, 120 | { 121 | "identity" : "swift-nio-http2", 122 | "kind" : "remoteSourceControl", 123 | "location" : "https://github.com/apple/swift-nio-http2.git", 124 | "state" : { 125 | "revision" : "5ca52e2f076c6a24451175f575f390569381d6a1", 126 | "version" : "1.37.0" 127 | } 128 | }, 129 | { 130 | "identity" : "swift-nio-ssl", 131 | "kind" : "remoteSourceControl", 132 | "location" : "https://github.com/apple/swift-nio-ssl.git", 133 | "state" : { 134 | "revision" : "36b48956eb6c0569215dc15a587b491d2bb36122", 135 | "version" : "2.32.0" 136 | } 137 | }, 138 | { 139 | "identity" : "swift-nio-transport-services", 140 | "kind" : "remoteSourceControl", 141 | "location" : "https://github.com/apple/swift-nio-transport-services.git", 142 | "state" : { 143 | "revision" : "decfd235996bc163b44e10b8a24997a3d2104b90", 144 | "version" : "1.25.0" 145 | } 146 | }, 147 | { 148 | "identity" : "swift-numerics", 149 | "kind" : "remoteSourceControl", 150 | "location" : "https://github.com/apple/swift-numerics.git", 151 | "state" : { 152 | "revision" : "e0ec0f5f3af6f3e4d5e7a19d2af26b481acb6ba8", 153 | "version" : "1.0.3" 154 | } 155 | }, 156 | { 157 | "identity" : "swift-service-lifecycle", 158 | "kind" : "remoteSourceControl", 159 | "location" : "https://github.com/swift-server/swift-service-lifecycle.git", 160 | "state" : { 161 | "revision" : "e7187309187695115033536e8fc9b2eb87fd956d", 162 | "version" : "2.8.0" 163 | } 164 | }, 165 | { 166 | "identity" : "swift-system", 167 | "kind" : "remoteSourceControl", 168 | "location" : "https://github.com/apple/swift-system.git", 169 | "state" : { 170 | "revision" : "61e4ca4b81b9e09e2ec863b00c340eb13497dac6", 171 | "version" : "1.5.0" 172 | } 173 | } 174 | ], 175 | "version" : 2 176 | } 177 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "025475604e8b0bc96fdab94268bef57a6fe87d5ce7d45a59af7a2381dfce0824", 3 | "pins" : [ 4 | { 5 | "identity" : "async-http-client", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/swift-server/async-http-client.git", 8 | "state" : { 9 | "revision" : "60235983163d040f343a489f7e2e77c1918a8bd9", 10 | "version" : "1.26.1" 11 | } 12 | }, 13 | { 14 | "identity" : "swift-algorithms", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/apple/swift-algorithms.git", 17 | "state" : { 18 | "revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023", 19 | "version" : "1.2.1" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-asn1", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/apple/swift-asn1.git", 26 | "state" : { 27 | "revision" : "f70225981241859eb4aa1a18a75531d26637c8cc", 28 | "version" : "1.4.0" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-async-algorithms", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/apple/swift-async-algorithms.git", 35 | "state" : { 36 | "revision" : "042e1c4d9d19748c9c228f8d4ebc97bb1e339b0b", 37 | "version" : "1.0.4" 38 | } 39 | }, 40 | { 41 | "identity" : "swift-atomics", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/apple/swift-atomics.git", 44 | "state" : { 45 | "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", 46 | "version" : "1.3.0" 47 | } 48 | }, 49 | { 50 | "identity" : "swift-certificates", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/apple/swift-certificates.git", 53 | "state" : { 54 | "revision" : "870f4d5fe5fcfedc13f25d70e103150511746404", 55 | "version" : "1.11.0" 56 | } 57 | }, 58 | { 59 | "identity" : "swift-collections", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/apple/swift-collections.git", 62 | "state" : { 63 | "revision" : "c1805596154bb3a265fd91b8ac0c4433b4348fb0", 64 | "version" : "1.2.0" 65 | } 66 | }, 67 | { 68 | "identity" : "swift-crypto", 69 | "kind" : "remoteSourceControl", 70 | "location" : "https://github.com/apple/swift-crypto.git", 71 | "state" : { 72 | "revision" : "e8d6eba1fef23ae5b359c46b03f7d94be2f41fed", 73 | "version" : "3.12.3" 74 | } 75 | }, 76 | { 77 | "identity" : "swift-http-structured-headers", 78 | "kind" : "remoteSourceControl", 79 | "location" : "https://github.com/apple/swift-http-structured-headers.git", 80 | "state" : { 81 | "revision" : "db6eea3692638a65e2124990155cd220c2915903", 82 | "version" : "1.3.0" 83 | } 84 | }, 85 | { 86 | "identity" : "swift-http-types", 87 | "kind" : "remoteSourceControl", 88 | "location" : "https://github.com/apple/swift-http-types.git", 89 | "state" : { 90 | "revision" : "a0a57e949a8903563aba4615869310c0ebf14c03", 91 | "version" : "1.4.0" 92 | } 93 | }, 94 | { 95 | "identity" : "swift-log", 96 | "kind" : "remoteSourceControl", 97 | "location" : "https://github.com/apple/swift-log.git", 98 | "state" : { 99 | "revision" : "3d8596ed08bd13520157f0355e35caed215ffbfa", 100 | "version" : "1.6.3" 101 | } 102 | }, 103 | { 104 | "identity" : "swift-nio", 105 | "kind" : "remoteSourceControl", 106 | "location" : "https://github.com/apple/swift-nio.git", 107 | "state" : { 108 | "revision" : "ad6b5f17270a7008f60d35ec5378e6144a575162", 109 | "version" : "2.84.0" 110 | } 111 | }, 112 | { 113 | "identity" : "swift-nio-extras", 114 | "kind" : "remoteSourceControl", 115 | "location" : "https://github.com/apple/swift-nio-extras.git", 116 | "state" : { 117 | "revision" : "145db1962f4f33a4ea07a32e751d5217602eea29", 118 | "version" : "1.28.0" 119 | } 120 | }, 121 | { 122 | "identity" : "swift-nio-http2", 123 | "kind" : "remoteSourceControl", 124 | "location" : "https://github.com/apple/swift-nio-http2.git", 125 | "state" : { 126 | "revision" : "5ca52e2f076c6a24451175f575f390569381d6a1", 127 | "version" : "1.37.0" 128 | } 129 | }, 130 | { 131 | "identity" : "swift-nio-ssl", 132 | "kind" : "remoteSourceControl", 133 | "location" : "https://github.com/apple/swift-nio-ssl.git", 134 | "state" : { 135 | "revision" : "36b48956eb6c0569215dc15a587b491d2bb36122", 136 | "version" : "2.32.0" 137 | } 138 | }, 139 | { 140 | "identity" : "swift-nio-transport-services", 141 | "kind" : "remoteSourceControl", 142 | "location" : "https://github.com/apple/swift-nio-transport-services.git", 143 | "state" : { 144 | "revision" : "decfd235996bc163b44e10b8a24997a3d2104b90", 145 | "version" : "1.25.0" 146 | } 147 | }, 148 | { 149 | "identity" : "swift-numerics", 150 | "kind" : "remoteSourceControl", 151 | "location" : "https://github.com/apple/swift-numerics.git", 152 | "state" : { 153 | "revision" : "e0ec0f5f3af6f3e4d5e7a19d2af26b481acb6ba8", 154 | "version" : "1.0.3" 155 | } 156 | }, 157 | { 158 | "identity" : "swift-service-lifecycle", 159 | "kind" : "remoteSourceControl", 160 | "location" : "https://github.com/swift-server/swift-service-lifecycle.git", 161 | "state" : { 162 | "revision" : "e7187309187695115033536e8fc9b2eb87fd956d", 163 | "version" : "2.8.0" 164 | } 165 | }, 166 | { 167 | "identity" : "swift-system", 168 | "kind" : "remoteSourceControl", 169 | "location" : "https://github.com/apple/swift-system.git", 170 | "state" : { 171 | "revision" : "61e4ca4b81b9e09e2ec863b00c340eb13497dac6", 172 | "version" : "1.5.0" 173 | } 174 | } 175 | ], 176 | "version" : 3 177 | } 178 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExample/Skills/SkillsDemoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SkillsDemoView.swift 3 | // SwiftAnthropicExample 4 | // 5 | // Created by James Rochabrun on 10/25/25. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftAnthropic 10 | 11 | struct SkillsDemoView: View { 12 | 13 | @State var observable: SkillsDemoObservable 14 | 15 | var body: some View { 16 | ScrollView { 17 | VStack(alignment: .leading, spacing: 20) { 18 | 19 | // Info Section 20 | GroupBox { 21 | VStack(alignment: .leading, spacing: 8) { 22 | Text("Skills API Demo") 23 | .font(.headline) 24 | Text("Test the new Skills functionality including listing available skills and using them in messages.") 25 | .font(.caption) 26 | .foregroundStyle(.secondary) 27 | 28 | if let containerId = observable.containerId { 29 | Divider() 30 | Text("Container ID: \(containerId)") 31 | .font(.caption2) 32 | .foregroundStyle(.blue) 33 | } 34 | } 35 | } 36 | 37 | // Action Buttons 38 | VStack(spacing: 12) { 39 | Button(action: { 40 | Task { await observable.listSkills() } 41 | }) { 42 | Label("List Available Skills", systemImage: "list.bullet") 43 | .frame(maxWidth: .infinity) 44 | .padding() 45 | .background(Color.blue) 46 | .foregroundColor(.white) 47 | .cornerRadius(10) 48 | } 49 | .disabled(observable.isLoading) 50 | 51 | Button(action: { 52 | Task { await observable.createMessageWithSkill() } 53 | }) { 54 | Label("Create Budget with XLSX Skill", systemImage: "doc.text") 55 | .frame(maxWidth: .infinity) 56 | .padding() 57 | .background(Color.green) 58 | .foregroundColor(.white) 59 | .cornerRadius(10) 60 | } 61 | .disabled(observable.isLoading) 62 | 63 | Button(action: { 64 | Task { await observable.streamMessageWithSkill() } 65 | }) { 66 | Label("Stream Chart with XLSX Skill", systemImage: "chart.bar") 67 | .frame(maxWidth: .infinity) 68 | .padding() 69 | .background(Color.purple) 70 | .foregroundColor(.white) 71 | .cornerRadius(10) 72 | } 73 | .disabled(observable.isLoading) 74 | } 75 | 76 | // Response Section 77 | if observable.isLoading { 78 | ProgressView() 79 | .frame(maxWidth: .infinity) 80 | .padding() 81 | } 82 | 83 | if !observable.errorMessage.isEmpty { 84 | GroupBox { 85 | VStack(alignment: .leading) { 86 | Label("Error", systemImage: "exclamationmark.triangle") 87 | .font(.headline) 88 | .foregroundColor(.red) 89 | Text(observable.errorMessage) 90 | .font(.caption) 91 | .foregroundStyle(.secondary) 92 | } 93 | } 94 | .backgroundStyle(.red.opacity(0.1)) 95 | } 96 | 97 | if !observable.message.isEmpty { 98 | GroupBox { 99 | VStack(alignment: .leading, spacing: 8) { 100 | Label("Response", systemImage: "text.bubble") 101 | .font(.headline) 102 | 103 | ScrollView { 104 | Text(observable.message) 105 | .font(.system(.body, design: .monospaced)) 106 | .textSelection(.enabled) 107 | .frame(maxWidth: .infinity, alignment: .leading) 108 | } 109 | .frame(maxHeight: 400) 110 | } 111 | } 112 | } 113 | 114 | // Skills List 115 | if !observable.availableSkills.isEmpty { 116 | GroupBox { 117 | VStack(alignment: .leading, spacing: 12) { 118 | Label("Available Skills", systemImage: "square.stack.3d.up") 119 | .font(.headline) 120 | 121 | ForEach(observable.availableSkills, id: \.id) { skill in 122 | VStack(alignment: .leading, spacing: 4) { 123 | Text(skill.displayTitle ?? "No title") 124 | .font(.subheadline) 125 | .fontWeight(.medium) 126 | HStack { 127 | Text("ID: \(skill.id)") 128 | .font(.caption2) 129 | Spacer() 130 | Text(skill.source.uppercased()) 131 | .font(.caption2) 132 | .padding(.horizontal, 6) 133 | .padding(.vertical, 2) 134 | .background(skill.source == "anthropic" ? Color.blue.opacity(0.2) : Color.orange.opacity(0.2)) 135 | .cornerRadius(4) 136 | } 137 | .foregroundStyle(.secondary) 138 | } 139 | .padding(.vertical, 4) 140 | 141 | if skill.id != observable.availableSkills.last?.id { 142 | Divider() 143 | } 144 | } 145 | } 146 | } 147 | } 148 | 149 | Spacer() 150 | } 151 | .padding() 152 | } 153 | .navigationTitle("Skills Demo") 154 | .navigationBarTitleDisplayMode(.inline) 155 | } 156 | } 157 | 158 | #Preview { 159 | NavigationStack { 160 | SkillsDemoView( 161 | observable: .init( 162 | service: AnthropicServiceFactory.service( 163 | apiKey: "test-key", 164 | betaHeaders: ["skills-2025-10-02", "code-execution-2025-08-25"] 165 | ) 166 | ) 167 | ) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /Examples/SwiftAnthropicExample/SwiftAnthropicExample/FunctionCalling/MessageFunctionCallingDemoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageFunctionCallingDemoView.swift 3 | // 4 | // 5 | // Created by James Rochabrun on 4/4/24. 6 | // 7 | 8 | import Foundation 9 | import PhotosUI 10 | import SwiftAnthropic 11 | import SwiftUI 12 | 13 | enum FunctionCallDefinition: String, CaseIterable { 14 | 15 | case getWeather = "get_weather" 16 | // Add more functions if needed, parallel function calling is supported. 17 | 18 | var tool: MessageParameter.Tool { 19 | switch self { 20 | case .getWeather: 21 | return .function( 22 | name: self.rawValue, 23 | description: "Get the current weather in a given location", 24 | inputSchema: .init( 25 | type: .object, 26 | properties: [ 27 | "location": .init(type: .string, description: "The city and state, e.g. San Francisco, CA"), 28 | "unit": .init(type: .string, description: "The unit of temperature, either celsius or fahrenheit") 29 | ], 30 | required: ["location"])) 31 | } 32 | } 33 | } 34 | 35 | @MainActor 36 | struct MessageFunctionCallingDemoView: View { 37 | 38 | let observable: MessageFunctionCallingObservable 39 | @State private var selectedSegment: ChatConfig = .messageStream 40 | @State private var prompt = "" 41 | 42 | @State private var selectedItems: [PhotosPickerItem] = [] 43 | @State private var selectedImages: [Image] = [] 44 | @State private var selectedImagesEncoded: [String] = [] 45 | 46 | enum ChatConfig { 47 | case message 48 | case messageStream 49 | } 50 | 51 | var body: some View { 52 | ScrollView { 53 | VStack { 54 | Text("TOOL: Web search") 55 | picker 56 | Text(observable.errorMessage) 57 | .foregroundColor(.red) 58 | messageView 59 | } 60 | .padding() 61 | } 62 | .overlay( 63 | Group { 64 | if observable.isLoading { 65 | ProgressView() 66 | } else { 67 | EmptyView() 68 | } 69 | } 70 | ).safeAreaInset(edge: .bottom) { 71 | VStack(spacing: 0) { 72 | selectedImagesView 73 | textArea 74 | } 75 | } 76 | } 77 | 78 | var textArea: some View { 79 | HStack(spacing: 4) { 80 | TextField("Enter prompt", text: $prompt, axis: .vertical) 81 | .textFieldStyle(.roundedBorder) 82 | .padding() 83 | photoPicker 84 | Button { 85 | Task { 86 | 87 | let images: [MessageParameter.Message.Content.ContentObject] = selectedImagesEncoded.map { 88 | .image(.init(type: .base64, mediaType: .jpeg, data: $0)) 89 | } 90 | let text: [MessageParameter.Message.Content.ContentObject] = [.text(prompt)] 91 | 92 | let finalInput = images + text 93 | 94 | let messages = [MessageParameter.Message(role: .user, content: .list(finalInput))] 95 | 96 | prompt = "" 97 | 98 | let webSearchTool = MessageParameter.webSearch( 99 | maxUses: 5, 100 | allowedDomains: ["wikipedia.org"], 101 | userLocation: .sanFrancisco 102 | ) 103 | 104 | let parameters = MessageParameter( 105 | model: .claude35Sonnet, 106 | messages: messages, 107 | maxTokens: 1024, 108 | tools: [webSearchTool]) 109 | switch selectedSegment { 110 | case .message: 111 | try await observable.createMessage(parameters: parameters) 112 | case .messageStream: 113 | try await observable.streamMessage(parameters: parameters) 114 | } 115 | } 116 | } label: { 117 | Image(systemName: "paperplane") 118 | } 119 | .buttonStyle(.bordered) 120 | } 121 | .padding() 122 | } 123 | 124 | var picker: some View { 125 | Picker("Options", selection: $selectedSegment) { 126 | Text("Message").tag(ChatConfig.message) 127 | Text("Message Stream").tag(ChatConfig.messageStream) 128 | } 129 | .pickerStyle(SegmentedPickerStyle()) 130 | .padding() 131 | } 132 | 133 | var messageView: some View { 134 | VStack(spacing: 24) { 135 | HStack { 136 | Button("Cancel") { 137 | observable.cancelStream() 138 | } 139 | Button("Clear Message") { 140 | observable.clearMessage() 141 | } 142 | } 143 | Text(observable.message) 144 | if let toolResponse = observable.toolUse { 145 | Divider() 146 | VStack { 147 | Text("Tool use") 148 | .bold() 149 | Text("Name: \(toolResponse.name)") 150 | Text("ID: \(toolResponse.id)") 151 | if !toolResponse.inputDisplay.isEmpty { 152 | Text("Input: \(toolResponse.inputDisplay)") 153 | } 154 | } 155 | } 156 | 157 | if !observable.totalJson.isEmpty { 158 | VStack { 159 | Divider() 160 | Text("Stream response tool use Json.") 161 | Text(observable.totalJson) 162 | } 163 | } 164 | } 165 | .buttonStyle(.bordered) 166 | } 167 | 168 | var photoPicker: some View { 169 | PhotosPicker(selection: $selectedItems, matching: .images) { 170 | Image(systemName: "photo") 171 | } 172 | .onChange(of: selectedItems) { 173 | Task { 174 | selectedImages.removeAll() 175 | for item in selectedItems { 176 | 177 | if let data = try? await item.loadTransferable(type: Data.self) { 178 | if let uiImage = UIImage(data: data), let resizedImageData = uiImage.jpegData(compressionQuality: 0.7) { 179 | // Make sure the resized image is below the size limit 180 | // This is needed as Claude allows a max of 5Mb size per image. 181 | if resizedImageData.count < 5_242_880 { // 5 MB in bytes 182 | let base64String = resizedImageData.base64EncodedString() 183 | selectedImagesEncoded.append(base64String) 184 | let image = Image(uiImage: UIImage(data: resizedImageData)!) 185 | selectedImages.append(image) 186 | } else { 187 | // Handle the error - maybe resize to an even smaller size or show an error message to the user 188 | } 189 | } 190 | } 191 | } 192 | } 193 | } 194 | } 195 | 196 | var selectedImagesView: some View { 197 | HStack(spacing: 0) { 198 | ForEach(0.. (URLSession.AuthChallengeDisposition, URLCredential?) { 64 | return self.answerChallenge(challenge) 65 | } 66 | 67 | func urlSession( 68 | _ session: URLSession, 69 | didReceive challenge: URLAuthenticationChallenge 70 | ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { 71 | return self.answerChallenge(challenge) 72 | } 73 | 74 | private func answerChallenge( 75 | _ challenge: URLAuthenticationChallenge 76 | ) -> (URLSession.AuthChallengeDisposition, URLCredential?) { 77 | guard let secTrust = challenge.protectionSpace.serverTrust else { 78 | aiproxyLogger.error("Could not access the server's security space") 79 | return (.cancelAuthenticationChallenge, nil) 80 | } 81 | 82 | guard let certificate = getServerCert(secTrust: secTrust) else { 83 | aiproxyLogger.error("Could not access the server's TLS cert") 84 | return (.cancelAuthenticationChallenge, nil) 85 | } 86 | 87 | let serverPublicKey = SecCertificateCopyKey(certificate)! 88 | let serverPublicKeyData = SecKeyCopyExternalRepresentation(serverPublicKey, nil)! 89 | 90 | for publicKeyData in publicKeysAsData { 91 | if serverPublicKeyData as Data == publicKeyData { 92 | let credential = URLCredential(trust: secTrust) 93 | return (.useCredential, credential) 94 | } 95 | } 96 | return (.cancelAuthenticationChallenge, nil) 97 | } 98 | } 99 | 100 | // MARK: - Private 101 | private var publicKeysAsData: [Data] = { 102 | let newVal = publicKeysAsHex.map { publicKeyAsHex in 103 | let keyData = Data(publicKeyAsHex) 104 | 105 | let attributes: [String: Any] = [ 106 | kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, 107 | kSecAttrKeyClass as String: kSecAttrKeyClassPublic, 108 | kSecAttrKeySizeInBits as String: 256 109 | ] 110 | 111 | var error: Unmanaged? 112 | let publicKey = SecKeyCreateWithData(keyData as CFData, attributes as CFDictionary, &error)! 113 | 114 | let localPublicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil)! as Data 115 | 116 | if let error = error { 117 | print("Failed to create public key: \(error.takeRetainedValue() as Error)") 118 | fatalError() 119 | } 120 | return localPublicKeyData 121 | } 122 | return newVal 123 | }() 124 | 125 | private let publicKeysAsHex: [[UInt8]] = [ 126 | // live on api.aiproxy.com 127 | [ 128 | 0x04, 0x4a, 0x42, 0x12, 0xe7, 0xed, 0x36, 0xb4, 0xa9, 0x1f, 0x96, 0x7e, 0xcf, 0xbd, 0xe0, 129 | 0x9d, 0xea, 0x4b, 0xfb, 0xaf, 0xe7, 0xc6, 0x93, 0xf0, 0xbf, 0x92, 0x0f, 0x12, 0x7a, 0x22, 130 | 0x7d, 0x00, 0x77, 0x81, 0xa5, 0x06, 0x26, 0x06, 0x5c, 0x47, 0x8f, 0x57, 0xef, 0x41, 0x39, 131 | 0x0b, 0x3d, 0x41, 0x72, 0x68, 0x33, 0x86, 0x69, 0x14, 0x2a, 0x36, 0x4d, 0x74, 0x7d, 0xbc, 132 | 0x60, 0x91, 0xff, 0xcc, 0x29 133 | ], 134 | 135 | // live on api.aiproxy.pro 136 | [ 137 | 0x04, 0x25, 0xa2, 0xd1, 0x81, 0xc0, 0x38, 0xce, 0x57, 0xaa, 0x6e, 0xf0, 0x5a, 0xc3, 0x6a, 138 | 0xa7, 0xc4, 0x69, 0x69, 0xcb, 0xeb, 0x24, 0xe5, 0x20, 0x7d, 0x06, 0xcb, 0xc7, 0x49, 0xd5, 139 | 0x0c, 0xac, 0xe6, 0x96, 0xc5, 0xc9, 0x28, 0x00, 0x8e, 0x69, 0xff, 0x9d, 0x32, 0x01, 0x53, 140 | 0x74, 0xab, 0xfd, 0x46, 0x03, 0x32, 0xed, 0x93, 0x7f, 0x0f, 0xe9, 0xd9, 0xc3, 0xaf, 0xe7, 141 | 0xa5, 0xcb, 0xc1, 0x29, 0x35 142 | ], 143 | 144 | // live on beta-api.aiproxy.pro 145 | [ 146 | 0x04, 0xaf, 0xb2, 0xcc, 0xe2, 0x51, 0x92, 0xcf, 0xb8, 0x01, 0x25, 0xc1, 0xb8, 0xda, 0x29, 147 | 0x51, 0x9f, 0x91, 0x4c, 0xaa, 0x09, 0x66, 0x3d, 0x81, 0xd7, 0xad, 0x6f, 0xdb, 0x78, 0x10, 148 | 0xd4, 0xbe, 0xcd, 0x4f, 0xe3, 0xaf, 0x4f, 0xb6, 0xd2, 0xca, 0x85, 0xb6, 0xc7, 0x3e, 0xb4, 149 | 0x61, 0x62, 0xe1, 0xfc, 0x90, 0xd6, 0x84, 0x1f, 0x98, 0xca, 0x83, 0x60, 0x8b, 0x65, 0xcb, 150 | 0x1a, 0x57, 0x6e, 0x32, 0x35, 151 | ], 152 | 153 | // backup-EC-key-A.key 154 | [ 155 | 0x04, 0x2c, 0x25, 0x74, 0xbc, 0x7e, 0x18, 0x10, 0x27, 0xbd, 0x03, 0x56, 0x4a, 0x7b, 0x32, 156 | 0xd2, 0xc1, 0xb0, 0x2e, 0x58, 0x85, 0x9a, 0xb0, 0x7d, 0xcd, 0x7e, 0x23, 0x33, 0x88, 0x2f, 157 | 0xc0, 0xfe, 0xce, 0x2e, 0xbf, 0x36, 0x67, 0xc6, 0x81, 0xf6, 0x52, 0x2b, 0x9b, 0xaf, 0x97, 158 | 0x3c, 0xac, 0x00, 0x39, 0xd8, 0xcc, 0x43, 0x6b, 0x1d, 0x65, 0xa5, 0xad, 0xd1, 0x57, 0x4b, 159 | 0xad, 0xb1, 0x17, 0xd3, 0x10 160 | ], 161 | 162 | // backup-EC-key-B.key 163 | [ 164 | 0x04, 0x34, 0xae, 0x84, 0x94, 0xe9, 0x02, 0xf0, 0x78, 0x0e, 0xee, 0xe6, 0x4e, 0x39, 0x7f, 165 | 0xb4, 0x84, 0xf6, 0xec, 0x55, 0x20, 0x0d, 0x36, 0xe9, 0xa6, 0x44, 0x6b, 0x9b, 0xe1, 0xef, 166 | 0x19, 0xe7, 0x90, 0x5b, 0xf4, 0xa3, 0x29, 0xf3, 0x56, 0x7c, 0x60, 0x97, 0xf0, 0xc6, 0x61, 167 | 0x83, 0x31, 0x5d, 0x2d, 0xc9, 0xcc, 0x40, 0x43, 0xad, 0x81, 0x63, 0xfd, 0xcf, 0xe2, 0x8e, 168 | 0xfa, 0x07, 0x09, 0xf6, 0xf2 169 | ], 170 | 171 | // backup-EC-key-C.key 172 | [ 173 | 0x04, 0x84, 0x4e, 0x33, 0xc8, 0x60, 0xe7, 0x78, 0xaa, 0xa2, 0xb6, 0x0b, 0xcf, 0x7a, 0x52, 174 | 0x43, 0xd1, 0x6d, 0x58, 0xff, 0x17, 0xb8, 0xea, 0x8a, 0x39, 0x53, 0xfb, 0x8b, 0x66, 0x7d, 175 | 0x10, 0x39, 0x80, 0x2c, 0x8d, 0xc9, 0xc3, 0x34, 0x33, 0x98, 0x14, 0xeb, 0x88, 0x7b, 0xf5, 176 | 0x4d, 0x1f, 0x07, 0xae, 0x6a, 0x02, 0x6b, 0xf5, 0x9b, 0xa8, 0xc6, 0x55, 0x5c, 0x27, 0xcd, 177 | 0x1b, 0xc0, 0x27, 0x2d, 0x82 178 | ] 179 | 180 | ] 181 | 182 | private func getServerCert(secTrust: SecTrust) -> SecCertificate? { 183 | if #available(macOS 12.0, iOS 15.0, *) { 184 | guard let certs = SecTrustCopyCertificateChain(secTrust) as? [SecCertificate] else { 185 | return nil 186 | } 187 | return certs[0] 188 | } else { 189 | return SecTrustGetCertificateAtIndex(secTrust, 0); 190 | } 191 | } 192 | #endif 193 | -------------------------------------------------------------------------------- /Sources/Anthropic/Service/DefaultAnthropicService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultAnthropicService.swift 3 | // 4 | // 5 | // Created by James Rochabrun on 1/28/24. 6 | // 7 | 8 | import Foundation 9 | #if canImport(FoundationNetworking) 10 | import FoundationNetworking 11 | #endif 12 | 13 | struct DefaultAnthropicService: AnthropicService { 14 | 15 | let httpClient: HTTPClient 16 | let decoder: JSONDecoder 17 | let apiKey: String 18 | let apiVersion: String 19 | let basePath: String 20 | let betaHeaders: [String]? 21 | /// Set this flag to TRUE if you need to print request events in DEBUG builds. 22 | private let debugEnabled: Bool 23 | 24 | init( 25 | apiKey: String, 26 | apiVersion: String = "2023-06-01", 27 | basePath: String, 28 | betaHeaders: [String]?, 29 | httpClient: HTTPClient, 30 | debugEnabled: Bool) 31 | { 32 | self.httpClient = httpClient 33 | let decoderWithSnakeCaseStrategy = JSONDecoder() 34 | decoderWithSnakeCaseStrategy.keyDecodingStrategy = .convertFromSnakeCase 35 | self.decoder = decoderWithSnakeCaseStrategy 36 | self.apiKey = apiKey 37 | self.apiVersion = apiVersion 38 | self.basePath = basePath 39 | self.betaHeaders = betaHeaders 40 | self.debugEnabled = debugEnabled 41 | } 42 | 43 | // MARK: Message 44 | 45 | func createMessage( 46 | _ parameter: MessageParameter) 47 | async throws -> MessageResponse 48 | { 49 | var localParameter = parameter 50 | localParameter.stream = false 51 | let request = try AnthropicAPI(base: basePath, apiPath: .messages).request(apiKey: apiKey, version: apiVersion, method: HTTPMethod.post, params: localParameter, betaHeaders: betaHeaders) 52 | return try await fetch(type: MessageResponse.self, with: request, debugEnabled: debugEnabled) 53 | } 54 | 55 | func streamMessage( 56 | _ parameter: MessageParameter) 57 | async throws -> AsyncThrowingStream 58 | { 59 | var localParameter = parameter 60 | localParameter.stream = true 61 | let request = try AnthropicAPI(base: basePath, apiPath: .messages).request(apiKey: apiKey, version: apiVersion, method: HTTPMethod.post, params: localParameter, betaHeaders: betaHeaders) 62 | return try await fetchStream(type: MessageStreamResponse.self, with: request, debugEnabled: debugEnabled) 63 | } 64 | 65 | func countTokens( 66 | parameter: MessageTokenCountParameter) 67 | async throws -> MessageInputTokens 68 | { 69 | let request = try AnthropicAPI(base: basePath, apiPath: .countTokens).request(apiKey: apiKey, version: apiVersion, method: HTTPMethod.post, params: parameter, betaHeaders: betaHeaders) 70 | return try await fetch(type: MessageInputTokens.self, with: request, debugEnabled: debugEnabled) 71 | } 72 | 73 | /// "messages-2023-12-15" 74 | // MARK: Text Completion 75 | 76 | func createTextCompletion( 77 | _ parameter: TextCompletionParameter) 78 | async throws -> TextCompletionResponse 79 | { 80 | var localParameter = parameter 81 | localParameter.stream = false 82 | let request = try AnthropicAPI(base: basePath, apiPath: .textCompletions).request(apiKey: apiKey, version: apiVersion, method: HTTPMethod.post, params: localParameter) 83 | return try await fetch(type: TextCompletionResponse.self, with: request, debugEnabled: debugEnabled) 84 | } 85 | 86 | func createStreamTextCompletion( 87 | _ parameter: TextCompletionParameter) 88 | async throws -> AsyncThrowingStream 89 | { 90 | var localParameter = parameter 91 | localParameter.stream = true 92 | let request = try AnthropicAPI(base: basePath, apiPath: .textCompletions).request(apiKey: apiKey, version: apiVersion, method: HTTPMethod.post, params: localParameter) 93 | return try await fetchStream(type: TextCompletionStreamResponse.self, with: request, debugEnabled: debugEnabled) 94 | } 95 | 96 | // MARK: Skills Management 97 | 98 | func createSkill( 99 | _ parameter: SkillCreateParameter) 100 | async throws -> SkillResponse 101 | { 102 | let request = try AnthropicAPI(base: basePath, apiPath: .skills).multipartRequest( 103 | apiKey: apiKey, 104 | version: apiVersion, 105 | method: .post, 106 | displayTitle: parameter.displayTitle, 107 | files: parameter.files, 108 | betaHeaders: betaHeaders 109 | ) 110 | return try await fetch(type: SkillResponse.self, with: request, debugEnabled: debugEnabled) 111 | } 112 | 113 | func listSkills( 114 | parameter: ListSkillsParameter?) 115 | async throws -> ListSkillsResponse 116 | { 117 | var queryItems: [URLQueryItem] = [] 118 | if let page = parameter?.page { 119 | queryItems.append(URLQueryItem(name: "page", value: page)) 120 | } 121 | if let limit = parameter?.limit { 122 | queryItems.append(URLQueryItem(name: "limit", value: "\(limit)")) 123 | } 124 | if let source = parameter?.source { 125 | queryItems.append(URLQueryItem(name: "source", value: source.rawValue)) 126 | } 127 | 128 | let request = try AnthropicAPI(base: basePath, apiPath: .skills).request( 129 | apiKey: apiKey, 130 | version: apiVersion, 131 | method: .get, 132 | betaHeaders: betaHeaders, 133 | queryItems: queryItems 134 | ) 135 | return try await fetch(type: ListSkillsResponse.self, with: request, debugEnabled: debugEnabled) 136 | } 137 | 138 | func retrieveSkill( 139 | skillId: String) 140 | async throws -> SkillResponse 141 | { 142 | let request = try AnthropicAPI(base: basePath, apiPath: .skill(id: skillId)).request( 143 | apiKey: apiKey, 144 | version: apiVersion, 145 | method: .get, 146 | betaHeaders: betaHeaders 147 | ) 148 | return try await fetch(type: SkillResponse.self, with: request, debugEnabled: debugEnabled) 149 | } 150 | 151 | func deleteSkill( 152 | skillId: String) 153 | async throws 154 | { 155 | let request = try AnthropicAPI(base: basePath, apiPath: .skill(id: skillId)).request( 156 | apiKey: apiKey, 157 | version: apiVersion, 158 | method: .delete, 159 | betaHeaders: betaHeaders 160 | ) 161 | // For DELETE requests, we just need to check the response status 162 | let httpRequest = try HTTPRequest(from: request) 163 | let (_, response) = try await httpClient.data(for: httpRequest) 164 | 165 | guard response.statusCode == 200 || response.statusCode == 204 else { 166 | throw APIError.responseUnsuccessful(description: "Failed to delete skill: status code \(response.statusCode)") 167 | } 168 | } 169 | 170 | // MARK: Skill Versions 171 | 172 | func createSkillVersion( 173 | skillId: String, 174 | _ parameter: SkillVersionCreateParameter) 175 | async throws -> SkillVersionResponse 176 | { 177 | let request = try AnthropicAPI(base: basePath, apiPath: .skillVersions(skillId: skillId)).multipartRequest( 178 | apiKey: apiKey, 179 | version: apiVersion, 180 | method: .post, 181 | displayTitle: nil, 182 | files: parameter.files, 183 | betaHeaders: betaHeaders 184 | ) 185 | return try await fetch(type: SkillVersionResponse.self, with: request, debugEnabled: debugEnabled) 186 | } 187 | 188 | func listSkillVersions( 189 | skillId: String, 190 | parameter: ListSkillVersionsParameter?) 191 | async throws -> ListSkillVersionsResponse 192 | { 193 | var queryItems: [URLQueryItem] = [] 194 | if let page = parameter?.page { 195 | queryItems.append(URLQueryItem(name: "page", value: page)) 196 | } 197 | if let limit = parameter?.limit { 198 | queryItems.append(URLQueryItem(name: "limit", value: "\(limit)")) 199 | } 200 | 201 | let request = try AnthropicAPI(base: basePath, apiPath: .skillVersions(skillId: skillId)).request( 202 | apiKey: apiKey, 203 | version: apiVersion, 204 | method: .get, 205 | betaHeaders: betaHeaders, 206 | queryItems: queryItems 207 | ) 208 | return try await fetch(type: ListSkillVersionsResponse.self, with: request, debugEnabled: debugEnabled) 209 | } 210 | 211 | func retrieveSkillVersion( 212 | skillId: String, 213 | version: String) 214 | async throws -> SkillVersionResponse 215 | { 216 | let request = try AnthropicAPI(base: basePath, apiPath: .skillVersion(skillId: skillId, version: version)).request( 217 | apiKey: apiKey, 218 | version: apiVersion, 219 | method: .get, 220 | betaHeaders: betaHeaders 221 | ) 222 | return try await fetch(type: SkillVersionResponse.self, with: request, debugEnabled: debugEnabled) 223 | } 224 | 225 | func deleteSkillVersion( 226 | skillId: String, 227 | version: String) 228 | async throws 229 | { 230 | let request = try AnthropicAPI(base: basePath, apiPath: .skillVersion(skillId: skillId, version: version)).request( 231 | apiKey: apiKey, 232 | version: apiVersion, 233 | method: .delete, 234 | betaHeaders: betaHeaders 235 | ) 236 | // For DELETE requests, we just need to check the response status 237 | let httpRequest = try HTTPRequest(from: request) 238 | let (_, response) = try await httpClient.data(for: httpRequest) 239 | 240 | guard response.statusCode == 200 || response.statusCode == 204 else { 241 | throw APIError.responseUnsuccessful(description: "Failed to delete skill version: status code \(response.statusCode)") 242 | } 243 | } 244 | 245 | } 246 | -------------------------------------------------------------------------------- /Sources/Anthropic/AIProxy/Endpoint+AIProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint+AIProxy.swift 3 | // 4 | // 5 | // Created by Lou Zell on 3/26/24. 6 | // 7 | 8 | #if !os(Linux) 9 | import Foundation 10 | import OSLog 11 | import DeviceCheck 12 | #if canImport(UIKit) 13 | import UIKit 14 | #endif 15 | #if canImport(IOKit) 16 | import IOKit 17 | #endif 18 | #if os(watchOS) 19 | import WatchKit 20 | #endif 21 | 22 | private let aiproxyLogger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "UnknownApp", 23 | category: "SwiftAnthropic+AIProxy") 24 | 25 | private let deviceCheckWarning = """ 26 | AIProxy warning: DeviceCheck is not available on this device. 27 | 28 | To use AIProxy on an iOS simulator, set an AIPROXY_DEVICE_CHECK_BYPASS environment variable. 29 | 30 | See the AIProxy section of the README at https://github.com/jamesrochabrun/SwiftAnthropic for instructions. 31 | """ 32 | 33 | 34 | // MARK: Endpoint+AIProxy 35 | extension Endpoint { 36 | 37 | func request( 38 | aiproxyPartialKey: String, 39 | clientID: String?, 40 | version: String, 41 | method: HTTPMethod, 42 | params: Encodable? = nil, 43 | betaHeaders: [String]? = nil, 44 | queryItems: [URLQueryItem] = []) 45 | async throws -> URLRequest 46 | { 47 | var request = URLRequest(url: urlComponents(queryItems: queryItems).url!) 48 | 49 | request.addValue(aiproxyPartialKey, forHTTPHeaderField: "aiproxy-partial-key") 50 | if let clientID = clientID ?? getClientID() { 51 | request.addValue(clientID, forHTTPHeaderField: "aiproxy-client-id") 52 | } 53 | if let deviceCheckToken = await getDeviceCheckToken() { 54 | request.addValue(deviceCheckToken, forHTTPHeaderField: "aiproxy-devicecheck") 55 | } 56 | #if DEBUG && targetEnvironment(simulator) 57 | if let deviceCheckBypass = ProcessInfo.processInfo.environment["AIPROXY_DEVICE_CHECK_BYPASS"] { 58 | request.addValue(deviceCheckBypass, forHTTPHeaderField: "aiproxy-devicecheck-bypass") 59 | } 60 | #endif 61 | 62 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 63 | request.addValue("\(version)", forHTTPHeaderField: "anthropic-version") 64 | if let betaHeaders { 65 | request.addValue("\(betaHeaders.joined(separator: ","))", forHTTPHeaderField: "anthropic-beta") 66 | } 67 | request.httpMethod = method.rawValue 68 | if let params { 69 | let encoder = JSONEncoder() 70 | encoder.keyEncodingStrategy = .convertToSnakeCase 71 | request.httpBody = try encoder.encode(params) 72 | } 73 | return request 74 | } 75 | 76 | /// Creates a multipart/form-data request for uploading skill files via AIProxy. 77 | /// 78 | /// This method is used for Skills API endpoints that require file uploads. 79 | func multipartRequest( 80 | aiproxyPartialKey: String, 81 | clientID: String?, 82 | version: String, 83 | method: HTTPMethod, 84 | displayTitle: String?, 85 | files: [SkillFile], 86 | betaHeaders: [String]? = nil, 87 | queryItems: [URLQueryItem] = []) 88 | async throws -> URLRequest 89 | { 90 | let boundary = "Boundary-\(UUID().uuidString)" 91 | var request = URLRequest(url: urlComponents(queryItems: queryItems).url!) 92 | 93 | request.addValue(aiproxyPartialKey, forHTTPHeaderField: "aiproxy-partial-key") 94 | if let clientID = clientID ?? getClientID() { 95 | request.addValue(clientID, forHTTPHeaderField: "aiproxy-client-id") 96 | } 97 | if let deviceCheckToken = await getDeviceCheckToken() { 98 | request.addValue(deviceCheckToken, forHTTPHeaderField: "aiproxy-devicecheck") 99 | } 100 | #if DEBUG && targetEnvironment(simulator) 101 | if let deviceCheckBypass = ProcessInfo.processInfo.environment["AIPROXY_DEVICE_CHECK_BYPASS"] { 102 | request.addValue(deviceCheckBypass, forHTTPHeaderField: "aiproxy-devicecheck-bypass") 103 | } 104 | #endif 105 | 106 | request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") 107 | request.addValue("\(version)", forHTTPHeaderField: "anthropic-version") 108 | if let betaHeaders { 109 | request.addValue("\(betaHeaders.joined(separator: ","))", forHTTPHeaderField: "anthropic-beta") 110 | } 111 | request.httpMethod = method.rawValue 112 | 113 | // Build multipart body 114 | var body = Data() 115 | 116 | // Add display_title if provided 117 | if let displayTitle = displayTitle { 118 | body.append("--\(boundary)\r\n".data(using: .utf8)!) 119 | body.append("Content-Disposition: form-data; name=\"display_title\"\r\n\r\n".data(using: .utf8)!) 120 | body.append("\(displayTitle)\r\n".data(using: .utf8)!) 121 | } 122 | 123 | // Add files 124 | for file in files { 125 | body.append("--\(boundary)\r\n".data(using: .utf8)!) 126 | 127 | let contentDisposition = "Content-Disposition: form-data; name=\"files[]\"; filename=\"\(file.filename)\"\r\n" 128 | body.append(contentDisposition.data(using: .utf8)!) 129 | 130 | if let mimeType = file.mimeType { 131 | body.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!) 132 | } else { 133 | body.append("\r\n".data(using: .utf8)!) 134 | } 135 | 136 | body.append(file.data) 137 | body.append("\r\n".data(using: .utf8)!) 138 | } 139 | 140 | // Close boundary 141 | body.append("--\(boundary)--\r\n".data(using: .utf8)!) 142 | 143 | request.httpBody = body 144 | return request 145 | } 146 | } 147 | 148 | 149 | // MARK: Private Helpers 150 | 151 | /// Gets a device check token for use in your calls to aiproxy. 152 | /// The device token may be nil when targeting the iOS simulator. 153 | private func getDeviceCheckToken() async -> String? { 154 | guard DCDevice.current.isSupported else { 155 | if ProcessInfo.processInfo.environment["AIPROXY_DEVICE_CHECK_BYPASS"] == nil { 156 | aiproxyLogger.warning("\(deviceCheckWarning, privacy: .public)") 157 | } 158 | return nil 159 | } 160 | 161 | do { 162 | let data = try await DCDevice.current.generateToken() 163 | return data.base64EncodedString() 164 | } catch { 165 | aiproxyLogger.error("Could not create DeviceCheck token. Are you using an explicit bundle identifier?") 166 | return nil 167 | } 168 | } 169 | 170 | /// Get a unique ID for this client 171 | private func getClientID() -> String? { 172 | #if os(watchOS) 173 | return WKInterfaceDevice.current().identifierForVendor?.uuidString 174 | #elseif canImport(UIKit) 175 | return UIDevice.current.identifierForVendor?.uuidString 176 | #elseif canImport(IOKit) 177 | return getIdentifierFromIOKit() 178 | #else 179 | return nil 180 | #endif 181 | } 182 | 183 | 184 | // MARK: IOKit conditional dependency 185 | /// These functions are used on macOS for creating a client identifier. 186 | /// Unfortunately, macOS does not have a straightforward helper like UIKit's `identifierForVendor` 187 | #if canImport(IOKit) 188 | private func getIdentifierFromIOKit() -> String? { 189 | guard let macBytes = copy_mac_address() as? Data else { 190 | return nil 191 | } 192 | let macHex = macBytes.map { String(format: "%02X", $0) } 193 | return macHex.joined(separator: ":") 194 | } 195 | 196 | // This function is taken from the Apple sample code at: 197 | // https://developer.apple.com/documentation/appstorereceipts/validating_receipts_on_the_device#3744656 198 | private func io_service(named name: String, wantBuiltIn: Bool) -> io_service_t? { 199 | let default_port = kIOMainPortDefault 200 | var iterator = io_iterator_t() 201 | defer { 202 | if iterator != IO_OBJECT_NULL { 203 | IOObjectRelease(iterator) 204 | } 205 | } 206 | 207 | guard let matchingDict = IOBSDNameMatching(default_port, 0, name), 208 | IOServiceGetMatchingServices(default_port, 209 | matchingDict as CFDictionary, 210 | &iterator) == KERN_SUCCESS, 211 | iterator != IO_OBJECT_NULL 212 | else { 213 | return nil 214 | } 215 | 216 | var candidate = IOIteratorNext(iterator) 217 | while candidate != IO_OBJECT_NULL { 218 | if let cftype = IORegistryEntryCreateCFProperty(candidate, 219 | "IOBuiltin" as CFString, 220 | kCFAllocatorDefault, 221 | 0) { 222 | let isBuiltIn = cftype.takeRetainedValue() as! CFBoolean 223 | if wantBuiltIn == CFBooleanGetValue(isBuiltIn) { 224 | return candidate 225 | } 226 | } 227 | 228 | IOObjectRelease(candidate) 229 | candidate = IOIteratorNext(iterator) 230 | } 231 | 232 | return nil 233 | } 234 | 235 | // This function is taken from the Apple sample code at: 236 | // https://developer.apple.com/documentation/appstorereceipts/validating_receipts_on_the_device#3744656 237 | private func copy_mac_address() -> CFData? { 238 | // Prefer built-in network interfaces. 239 | // For example, an external Ethernet adaptor can displace 240 | // the built-in Wi-Fi as en0. 241 | guard let service = io_service(named: "en0", wantBuiltIn: true) 242 | ?? io_service(named: "en1", wantBuiltIn: true) 243 | ?? io_service(named: "en0", wantBuiltIn: false) 244 | else { return nil } 245 | defer { IOObjectRelease(service) } 246 | 247 | if let cftype = IORegistryEntrySearchCFProperty( 248 | service, 249 | kIOServicePlane, 250 | "IOMACAddress" as CFString, 251 | kCFAllocatorDefault, 252 | IOOptionBits(kIORegistryIterateRecursively | kIORegistryIterateParents)) { 253 | return (cftype as! CFData) 254 | } 255 | 256 | return nil 257 | } 258 | #endif 259 | #endif 260 | -------------------------------------------------------------------------------- /Sources/Anthropic/AIProxy/AIProxyService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AIProxyService.swift 3 | // 4 | // 5 | // Created by Lou Zell on 7/31/24. 6 | // 7 | 8 | #if !os(Linux) 9 | import Foundation 10 | 11 | private let aiproxySecureDelegate = AIProxyCertificatePinningDelegate() 12 | 13 | 14 | struct AIProxyService: AnthropicService { 15 | 16 | let httpClient: HTTPClient 17 | let decoder: JSONDecoder 18 | 19 | /// Your partial key is provided during the integration process at dashboard.aiproxy.pro 20 | /// Please see the [integration guide](https://www.aiproxy.pro/docs/integration-guide.html) for acquiring your partial key 21 | private let partialKey: String 22 | 23 | /// Your service URL is also provided during the integration process. 24 | private let serviceURL: String 25 | 26 | /// Optionally supply your own client IDs to annotate requests with in the AIProxy developer dashboard. 27 | /// It is safe to leave this blank (most people do). If you leave it blank, AIProxy generates client IDs for you. 28 | private let clientID: String? 29 | 30 | /// Set this flag to TRUE if you need to print request events in DEBUG builds. 31 | private let debugEnabled: Bool 32 | 33 | /// Defaults to "2023-06-01" 34 | private var apiVersion: String 35 | 36 | private let betaHeaders: [String]? 37 | 38 | init( 39 | partialKey: String, 40 | serviceURL: String, 41 | clientID: String? = nil, 42 | apiVersion: String = "2023-06-01", 43 | betaHeaders: [String]?, 44 | debugEnabled: Bool) 45 | { 46 | let decoderWithSnakeCaseStrategy = JSONDecoder() 47 | decoderWithSnakeCaseStrategy.keyDecodingStrategy = .convertFromSnakeCase 48 | self.decoder = decoderWithSnakeCaseStrategy 49 | self.partialKey = partialKey 50 | self.serviceURL = serviceURL 51 | self.clientID = clientID 52 | self.apiVersion = apiVersion 53 | self.betaHeaders = betaHeaders 54 | self.debugEnabled = debugEnabled 55 | self.httpClient = URLSessionHTTPClientAdapter( 56 | urlSession: URLSession( 57 | configuration: .default, 58 | delegate: aiproxySecureDelegate, 59 | delegateQueue: nil 60 | ) 61 | ) 62 | } 63 | 64 | // MARK: Message 65 | 66 | func createMessage( 67 | _ parameter: MessageParameter) 68 | async throws -> MessageResponse 69 | { 70 | var localParameter = parameter 71 | localParameter.stream = false 72 | let request = try await AnthropicAPI(base: serviceURL, apiPath: .messages).request(aiproxyPartialKey: partialKey, clientID: clientID, version: apiVersion, method: .post, params: localParameter, betaHeaders: betaHeaders) 73 | return try await fetch(type: MessageResponse.self, with: request, debugEnabled: debugEnabled) 74 | } 75 | 76 | func streamMessage( 77 | _ parameter: MessageParameter) 78 | async throws -> AsyncThrowingStream 79 | { 80 | var localParameter = parameter 81 | localParameter.stream = true 82 | let request = try await AnthropicAPI(base: serviceURL, apiPath: .messages).request(aiproxyPartialKey: partialKey, clientID: clientID, version: apiVersion, method: .post, params: localParameter, betaHeaders: betaHeaders) 83 | return try await fetchStream(type: MessageStreamResponse.self, with: request, debugEnabled: debugEnabled) 84 | } 85 | 86 | func countTokens( 87 | parameter: MessageTokenCountParameter) 88 | async throws -> MessageInputTokens 89 | { 90 | let request = try await AnthropicAPI(base: serviceURL, apiPath: .countTokens).request(aiproxyPartialKey: partialKey, clientID: clientID, version: apiVersion, method: .post, params: parameter, betaHeaders: betaHeaders) 91 | return try await fetch(type: MessageInputTokens.self, with: request, debugEnabled: debugEnabled) 92 | } 93 | 94 | // MARK: Text Completion 95 | 96 | func createTextCompletion( 97 | _ parameter: TextCompletionParameter) 98 | async throws -> TextCompletionResponse 99 | { 100 | var localParameter = parameter 101 | localParameter.stream = false 102 | let request = try await AnthropicAPI(base: serviceURL, apiPath: .textCompletions).request(aiproxyPartialKey: partialKey, clientID: clientID, version: apiVersion, method: .post, params: localParameter) 103 | return try await fetch(type: TextCompletionResponse.self, with: request, debugEnabled: debugEnabled) 104 | } 105 | 106 | func createStreamTextCompletion( 107 | _ parameter: TextCompletionParameter) 108 | async throws -> AsyncThrowingStream 109 | { 110 | var localParameter = parameter 111 | localParameter.stream = true 112 | let request = try await AnthropicAPI(base: serviceURL, apiPath: .textCompletions).request(aiproxyPartialKey: partialKey, clientID: clientID, version: apiVersion, method: .post, params: localParameter) 113 | return try await fetchStream(type: TextCompletionStreamResponse.self, with: request, debugEnabled: debugEnabled) 114 | } 115 | 116 | // MARK: Skills Management 117 | 118 | func createSkill( 119 | _ parameter: SkillCreateParameter) 120 | async throws -> SkillResponse 121 | { 122 | let request = try await AnthropicAPI(base: serviceURL, apiPath: .skills).multipartRequest( 123 | aiproxyPartialKey: partialKey, 124 | clientID: clientID, 125 | version: apiVersion, 126 | method: .post, 127 | displayTitle: parameter.displayTitle, 128 | files: parameter.files, 129 | betaHeaders: betaHeaders 130 | ) 131 | return try await fetch(type: SkillResponse.self, with: request, debugEnabled: debugEnabled) 132 | } 133 | 134 | func listSkills( 135 | parameter: ListSkillsParameter?) 136 | async throws -> ListSkillsResponse 137 | { 138 | var queryItems: [URLQueryItem] = [] 139 | if let page = parameter?.page { 140 | queryItems.append(URLQueryItem(name: "page", value: page)) 141 | } 142 | if let limit = parameter?.limit { 143 | queryItems.append(URLQueryItem(name: "limit", value: "\(limit)")) 144 | } 145 | if let source = parameter?.source { 146 | queryItems.append(URLQueryItem(name: "source", value: source.rawValue)) 147 | } 148 | 149 | let request = try await AnthropicAPI(base: serviceURL, apiPath: .skills).request( 150 | aiproxyPartialKey: partialKey, 151 | clientID: clientID, 152 | version: apiVersion, 153 | method: .get, 154 | betaHeaders: betaHeaders, 155 | queryItems: queryItems 156 | ) 157 | return try await fetch(type: ListSkillsResponse.self, with: request, debugEnabled: debugEnabled) 158 | } 159 | 160 | func retrieveSkill( 161 | skillId: String) 162 | async throws -> SkillResponse 163 | { 164 | let request = try await AnthropicAPI(base: serviceURL, apiPath: .skill(id: skillId)).request( 165 | aiproxyPartialKey: partialKey, 166 | clientID: clientID, 167 | version: apiVersion, 168 | method: .get, 169 | betaHeaders: betaHeaders 170 | ) 171 | return try await fetch(type: SkillResponse.self, with: request, debugEnabled: debugEnabled) 172 | } 173 | 174 | func deleteSkill( 175 | skillId: String) 176 | async throws 177 | { 178 | let request = try await AnthropicAPI(base: serviceURL, apiPath: .skill(id: skillId)).request( 179 | aiproxyPartialKey: partialKey, 180 | clientID: clientID, 181 | version: apiVersion, 182 | method: .delete, 183 | betaHeaders: betaHeaders 184 | ) 185 | let httpRequest = try HTTPRequest(from: request) 186 | let (_, response) = try await httpClient.data(for: httpRequest) 187 | 188 | guard response.statusCode == 200 || response.statusCode == 204 else { 189 | throw APIError.responseUnsuccessful(description: "Failed to delete skill: status code \(response.statusCode)") 190 | } 191 | } 192 | 193 | // MARK: Skill Versions 194 | 195 | func createSkillVersion( 196 | skillId: String, 197 | _ parameter: SkillVersionCreateParameter) 198 | async throws -> SkillVersionResponse 199 | { 200 | let request = try await AnthropicAPI(base: serviceURL, apiPath: .skillVersions(skillId: skillId)).multipartRequest( 201 | aiproxyPartialKey: partialKey, 202 | clientID: clientID, 203 | version: apiVersion, 204 | method: .post, 205 | displayTitle: nil, 206 | files: parameter.files, 207 | betaHeaders: betaHeaders 208 | ) 209 | return try await fetch(type: SkillVersionResponse.self, with: request, debugEnabled: debugEnabled) 210 | } 211 | 212 | func listSkillVersions( 213 | skillId: String, 214 | parameter: ListSkillVersionsParameter?) 215 | async throws -> ListSkillVersionsResponse 216 | { 217 | var queryItems: [URLQueryItem] = [] 218 | if let page = parameter?.page { 219 | queryItems.append(URLQueryItem(name: "page", value: page)) 220 | } 221 | if let limit = parameter?.limit { 222 | queryItems.append(URLQueryItem(name: "limit", value: "\(limit)")) 223 | } 224 | 225 | let request = try await AnthropicAPI(base: serviceURL, apiPath: .skillVersions(skillId: skillId)).request( 226 | aiproxyPartialKey: partialKey, 227 | clientID: clientID, 228 | version: apiVersion, 229 | method: .get, 230 | betaHeaders: betaHeaders, 231 | queryItems: queryItems 232 | ) 233 | return try await fetch(type: ListSkillVersionsResponse.self, with: request, debugEnabled: debugEnabled) 234 | } 235 | 236 | func retrieveSkillVersion( 237 | skillId: String, 238 | version: String) 239 | async throws -> SkillVersionResponse 240 | { 241 | let request = try await AnthropicAPI(base: serviceURL, apiPath: .skillVersion(skillId: skillId, version: version)).request( 242 | aiproxyPartialKey: partialKey, 243 | clientID: clientID, 244 | version: apiVersion, 245 | method: .get, 246 | betaHeaders: betaHeaders 247 | ) 248 | return try await fetch(type: SkillVersionResponse.self, with: request, debugEnabled: debugEnabled) 249 | } 250 | 251 | func deleteSkillVersion( 252 | skillId: String, 253 | version: String) 254 | async throws 255 | { 256 | let request = try await AnthropicAPI(base: serviceURL, apiPath: .skillVersion(skillId: skillId, version: version)).request( 257 | aiproxyPartialKey: partialKey, 258 | clientID: clientID, 259 | version: apiVersion, 260 | method: .delete, 261 | betaHeaders: betaHeaders 262 | ) 263 | let httpRequest = try HTTPRequest(from: request) 264 | let (_, response) = try await httpClient.data(for: httpRequest) 265 | 266 | guard response.statusCode == 200 || response.statusCode == 204 else { 267 | throw APIError.responseUnsuccessful(description: "Failed to delete skill version: status code \(response.statusCode)") 268 | } 269 | } 270 | } 271 | #endif 272 | -------------------------------------------------------------------------------- /Sources/Anthropic/Public/StreamHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StreamHandler.swift 3 | // SwiftAnthropic 4 | // 5 | // Created by James Rochabrun on 2/24/25. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class StreamHandler { 11 | 12 | public init() {} 13 | 14 | // Process a stream event 15 | public func handleStreamEvent(_ event: MessageStreamResponse) { 16 | // First identify event type 17 | switch event.streamEvent { 18 | case .contentBlockStart: 19 | handleContentBlockStart(event) 20 | case .contentBlockDelta: 21 | handleContentBlockDelta(event) 22 | case .contentBlockStop: 23 | handleContentBlockStop() 24 | case .messageStart: 25 | // Just initialize as needed 26 | debugPrint("Stream started") 27 | case .messageDelta, .messageStop: 28 | // Handle message completion 29 | if event.streamEvent == .messageStop { 30 | debugPrint("\nStream complete!") 31 | printSummary() 32 | } 33 | case .none: 34 | debugPrint("Unknown event type: \(event.type)") 35 | } 36 | } 37 | 38 | // Get all content blocks for use in subsequent API calls 39 | public func getContentBlocksForAPI() -> [MessageParameter.Message.Content.ContentObject] { 40 | var blocks: [MessageParameter.Message.Content.ContentObject] = [] 41 | 42 | // Add regular thinking blocks 43 | for block in thinkingBlocks { 44 | if let signature = block.signature { 45 | blocks.append(.thinking(block.thinking, signature)) 46 | } 47 | } 48 | 49 | // Add redacted thinking blocks if any 50 | for data in redactedThinkingBlocks { 51 | blocks.append(.redactedThinking(data)) 52 | } 53 | 54 | // Add tool use blocks if any 55 | for toolUse in toolUseBlocks { 56 | blocks.append(.toolUse(toolUse.id, toolUse.name, toolUse.input)) 57 | } 58 | 59 | return blocks 60 | } 61 | 62 | // Get only thinking blocks for use in subsequent API calls 63 | public func getThinkingBlocksForAPI() -> [MessageParameter.Message.Content.ContentObject] { 64 | var blocks: [MessageParameter.Message.Content.ContentObject] = [] 65 | 66 | // Add regular thinking blocks 67 | for block in thinkingBlocks { 68 | if let signature = block.signature { 69 | blocks.append(.thinking(block.thinking, signature)) 70 | } 71 | } 72 | 73 | // Add redacted thinking blocks if any 74 | for data in redactedThinkingBlocks { 75 | blocks.append(.redactedThinking(data)) 76 | } 77 | 78 | return blocks 79 | } 80 | 81 | // Get text response content 82 | public var textResponse: String { 83 | return currentResponse 84 | } 85 | 86 | // Get tool use blocks 87 | public func getToolUseBlocks() -> [ToolUseBlock] { 88 | return toolUseBlocks 89 | } 90 | 91 | // Get accumulated JSON for a specific tool use ID 92 | public func getAccumulatedJson(forToolUseId id: String) -> String? { 93 | return toolUseJsonMap[id] 94 | } 95 | 96 | // Current thinking content being collected 97 | private var currentThinking = "" 98 | // Current signature being collected 99 | private var signature: String? 100 | // Current text response being collected 101 | private var currentResponse = "" 102 | // Current tool use block being collected 103 | private var currentToolUse: ToolUseBlock? 104 | // Accumulated JSON for the current tool use 105 | private var currentToolUseJson = "" 106 | 107 | // Track the current active content block index and type 108 | private var currentBlockIndex: Int? = nil 109 | private var currentBlockType: String? = nil 110 | 111 | // Store all collected thinking blocks 112 | private var thinkingBlocks: [(thinking: String, signature: String?)] = [] 113 | // Stored redacted thinking blocks 114 | private var redactedThinkingBlocks: [String] = [] 115 | // Stored tool use blocks 116 | private var toolUseBlocks: [ToolUseBlock] = [] 117 | // Map of tool use IDs to their accumulated JSON 118 | private var toolUseJsonMap: [String: String] = [:] 119 | 120 | // Structure to store tool use information 121 | public struct ToolUseBlock { 122 | public let id: String 123 | public let name: String 124 | public let input: MessageResponse.Content.Input 125 | 126 | // Added for convenience 127 | public var accumulatedJson: String? 128 | 129 | public init(id: String, name: String, input: MessageResponse.Content.Input, accumulatedJson: String? = nil) { 130 | self.id = id 131 | self.name = name 132 | self.input = input 133 | self.accumulatedJson = accumulatedJson 134 | } 135 | } 136 | 137 | private func handleContentBlockStart(_ event: MessageStreamResponse) { 138 | guard let contentBlock = event.contentBlock, let index = event.index else { return } 139 | 140 | currentBlockIndex = index 141 | currentBlockType = contentBlock.type 142 | 143 | switch contentBlock.type { 144 | case "thinking": 145 | currentThinking = contentBlock.thinking ?? "" 146 | debugPrint("\nStarting thinking block...") 147 | case "redacted_thinking": 148 | if let data = contentBlock.data { 149 | redactedThinkingBlocks.append(data) 150 | debugPrint("\nEncountered redacted thinking block") 151 | } 152 | case "text": 153 | currentResponse = contentBlock.text ?? "" 154 | debugPrint("\nStarting text response...") 155 | case "tool_use": 156 | if let id = contentBlock.id, let name = contentBlock.name { 157 | // Initialize the JSON accumulator for this tool use 158 | currentToolUseJson = "" 159 | // Create the tool use block with initial input (may be empty) 160 | currentToolUse = ToolUseBlock(id: id, name: name, input: contentBlock.input ?? [:]) 161 | debugPrint("\nStarting tool use block: \(name) with ID: \(id)") 162 | } 163 | default: 164 | debugPrint("\nStarting \(contentBlock.type) block...") 165 | } 166 | } 167 | 168 | private func handleContentBlockDelta(_ event: MessageStreamResponse) { 169 | guard let delta = event.delta, let index = event.index else { return } 170 | 171 | // Ensure we're tracking the correct block 172 | if currentBlockIndex != index { 173 | currentBlockIndex = index 174 | } 175 | 176 | // Process based on delta type 177 | switch delta.type { 178 | case "thinking_delta": 179 | if let thinking = delta.thinking { 180 | currentThinking += thinking 181 | debugPrint(thinking, terminator: "") 182 | } 183 | case "signature_delta": 184 | if let sig = delta.signature { 185 | signature = sig 186 | debugPrint("\nReceived signature for thinking block") 187 | } 188 | case "text_delta": 189 | if let text = delta.text { 190 | currentResponse += text 191 | debugPrint(text, terminator: "") 192 | } 193 | case "tool_use_delta": 194 | if let partialJson = delta.partialJson, let currentId = currentToolUse?.id { 195 | // Accumulate the JSON 196 | currentToolUseJson += partialJson 197 | // Update the map 198 | toolUseJsonMap[currentId] = currentToolUseJson 199 | debugPrint("\nAccumulated tool use JSON for \(currentId): \(partialJson)") 200 | 201 | // Try to parse the accumulated JSON if it might be complete 202 | if isValidJson(currentToolUseJson) { 203 | debugPrint("\nValid JSON detected for tool use \(currentId)") 204 | // Here you could attempt to update the tool use input if needed 205 | updateToolUseInputIfPossible(toolUseId: currentId, json: currentToolUseJson) 206 | } 207 | } 208 | default: 209 | if let type = delta.type { 210 | debugPrint("\nUnknown delta type: \(type)") 211 | } 212 | } 213 | } 214 | 215 | private func handleContentBlockStop() { 216 | // When a block is complete, store it if needed 217 | if currentBlockType == "thinking" && !currentThinking.isEmpty { 218 | thinkingBlocks.append((thinking: currentThinking, signature: signature)) 219 | 220 | // Reset for next block 221 | currentThinking = "" 222 | signature = nil 223 | } else if currentBlockType == "tool_use" && currentToolUse != nil { 224 | if let toolUse = currentToolUse, let id = currentToolUse?.id { 225 | // Create a new ToolUseBlock with the accumulated JSON 226 | let updatedToolUse = ToolUseBlock( 227 | id: toolUse.id, 228 | name: toolUse.name, 229 | input: toolUse.input, 230 | accumulatedJson: toolUseJsonMap[id] 231 | ) 232 | 233 | toolUseBlocks.append(updatedToolUse) 234 | debugPrint("\nStored tool use block with ID: \(id) and accumulated JSON") 235 | } 236 | currentToolUse = nil 237 | currentToolUseJson = "" 238 | } 239 | 240 | // Reset tracking 241 | currentBlockType = nil 242 | } 243 | 244 | // Check if a string is valid JSON 245 | private func isValidJson(_ jsonString: String) -> Bool { 246 | guard !jsonString.isEmpty else { return false } 247 | return (try? JSONSerialization.jsonObject(with: Data(jsonString.utf8))) != nil 248 | } 249 | 250 | // Try to update the tool use input from accumulated JSON 251 | private func updateToolUseInputIfPossible(toolUseId: String, json: String) { 252 | // This would be implemented based on your specific needs 253 | // For example, you might decode the JSON and update the corresponding input 254 | // This is just a placeholder for where you would implement that logic 255 | debugPrint("\nWould update tool use input for \(toolUseId) based on JSON if implemented") 256 | } 257 | 258 | // Reset all stored data 259 | public func reset() { 260 | currentThinking = "" 261 | signature = nil 262 | currentResponse = "" 263 | currentToolUse = nil 264 | currentToolUseJson = "" 265 | currentBlockIndex = nil 266 | currentBlockType = nil 267 | thinkingBlocks.removeAll() 268 | redactedThinkingBlocks.removeAll() 269 | toolUseBlocks.removeAll() 270 | toolUseJsonMap.removeAll() 271 | } 272 | 273 | // Print a summary of what was collected 274 | private func printSummary() { 275 | debugPrint("\n\n===== SUMMARY =====") 276 | debugPrint("Number of thinking blocks: \(thinkingBlocks.count)") 277 | debugPrint("Number of redacted thinking blocks: \(redactedThinkingBlocks.count)") 278 | debugPrint("Number of tool use blocks: \(toolUseBlocks.count)") 279 | debugPrint("Number of tool use JSON objects: \(toolUseJsonMap.count)") 280 | debugPrint("Final response length: \(currentResponse.count) characters") 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /Sources/Anthropic/Service/AnthropicService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnthropicService.swift 3 | // 4 | // 5 | // Created by James Rochabrun on 1/28/24. 6 | // 7 | 8 | import Foundation 9 | #if os(Linux) 10 | import FoundationNetworking 11 | #endif 12 | 13 | // MARK: Error 14 | 15 | public enum APIError: Error { 16 | 17 | case requestFailed(description: String) 18 | case responseUnsuccessful(description: String) 19 | case invalidData 20 | case jsonDecodingFailure(description: String) 21 | case dataCouldNotBeReadMissingData(description: String) 22 | case bothDecodingStrategiesFailed 23 | case timeOutError 24 | 25 | public var displayDescription: String { 26 | switch self { 27 | case .requestFailed(let description): return description 28 | case .responseUnsuccessful(let description): return description 29 | case .invalidData: return "Invalid data" 30 | case .jsonDecodingFailure(let description): return description 31 | case .dataCouldNotBeReadMissingData(let description): return description 32 | case .bothDecodingStrategiesFailed: return "Decoding strategies failed." 33 | case .timeOutError: return "Time Out Error." 34 | } 35 | } 36 | } 37 | 38 | // MARK: Service 39 | 40 | /// A protocol defining the required services for interacting with Anthropic's API. 41 | /// 42 | /// The protocol outlines methods for fetching data and streaming responses, 43 | /// as well as handling JSON decoding and networking tasks. 44 | public protocol AnthropicService { 45 | 46 | /// The HTTP client responsible for executing all network requests. 47 | /// 48 | /// This client is used for tasks like sending and receiving data. 49 | var httpClient: HTTPClient { get } 50 | /// The `JSONDecoder` instance used for decoding JSON responses. 51 | /// 52 | /// This decoder is used to parse the JSON responses returned by the API 53 | /// into model objects that conform to the `Decodable` protocol. 54 | var decoder: JSONDecoder { get } 55 | 56 | // MARK: Message 57 | 58 | /// Creates a message with the provided parameters. 59 | /// 60 | /// - Parameters: 61 | /// - parameters: Parameters for the create message request. 62 | /// 63 | /// - Returns: A [MessageResponse](https://docs.anthropic.com/claude/reference/messages_post). 64 | /// 65 | /// - Throws: An error if the request fails. 66 | /// 67 | /// For more information, refer to [Anthropic's Message API documentation](https://docs.anthropic.com/claude/reference/messages_post). 68 | func createMessage( 69 | _ parameter: MessageParameter) 70 | async throws -> MessageResponse 71 | 72 | /// Creates a message stream with the provided parameters. 73 | /// 74 | /// - Parameters: 75 | /// - parameters: Parameters for the create message request. 76 | /// 77 | /// - Returns: A streamed sequence of `MessageStreamResponse`. 78 | /// For more details, see [MessageStreamResponse](https://docs.anthropic.com/claude/reference/messages-streaming). 79 | /// 80 | /// - Throws: An error if the request fails. 81 | /// 82 | /// For more information, refer to [Anthropic's Stream Message API documentation](https://docs.anthropic.com/claude/reference/messages-streaming). 83 | func streamMessage( 84 | _ parameter: MessageParameter) 85 | async throws -> AsyncThrowingStream 86 | 87 | /// Counts the number of tokens that would be used by a message for a given model. 88 | /// 89 | /// - Parameters: 90 | /// - parameter: The parameters used to count tokens, including the model, messages, system prompt, and tools. 91 | /// 92 | /// - Returns: A `MessageInputTokens` object containing the count of input tokens. 93 | /// 94 | /// - Throws: An error if the token counting request fails. 95 | /// 96 | /// Example usage: 97 | /// ```swift 98 | /// let parameter = MessageTokenCountParameter( 99 | /// model: .claude3Sonnet, 100 | /// messages: [ 101 | /// .init( 102 | /// role: .user, 103 | /// content: .text("Hello, Claude!") 104 | /// ) 105 | /// ] 106 | /// ) 107 | /// 108 | /// let tokenCount = try await client.countTokens(parameter: parameter) 109 | /// print("Input tokens: \(tokenCount.inputTokens)") 110 | /// ``` 111 | /// 112 | /// For more details, see [Count Message tokens](https://docs.anthropic.com/en/api/messages-count-tokens) 113 | func countTokens( 114 | parameter: MessageTokenCountParameter) 115 | async throws -> MessageInputTokens 116 | 117 | 118 | // MARK: Text Completion 119 | 120 | /// - Parameter parameters: Parameters for the create text completion request. 121 | /// - Returns: A [TextCompletionResponse](https://docs.anthropic.com/claude/reference/complete_post). 122 | /// - Throws: An error if the request fails. 123 | /// 124 | /// For more information, refer to [Anthropic's Text Completion API documentation](https://docs.anthropic.com/claude/reference/complete_post). 125 | func createTextCompletion( 126 | _ parameter: TextCompletionParameter) 127 | async throws -> TextCompletionResponse 128 | 129 | /// - Parameter parameters: Parameters for the create stream text completion request. 130 | /// - Returns: A [TextCompletionResponse](https://docs.anthropic.com/claude/reference/streaming). 131 | /// - Throws: An error if the request fails. 132 | /// 133 | /// For more information, refer to [Anthropic's Text Completion API documentation](https://docs.anthropic.com/claude/reference/streaming). 134 | func createStreamTextCompletion( 135 | _ parameter: TextCompletionParameter) 136 | async throws -> AsyncThrowingStream 137 | 138 | // MARK: Skills Management 139 | 140 | /// Creates a new skill by uploading skill files. 141 | /// 142 | /// - Parameter parameter: Parameters for creating the skill, including display title and files. 143 | /// 144 | /// - Returns: A [SkillResponse](https://docs.anthropic.com/claude/reference/skills/create-skill). 145 | /// 146 | /// - Throws: An error if the request fails. 147 | /// 148 | /// For more information, refer to [Anthropic's Skills API documentation](https://docs.anthropic.com/claude/reference/skills/create-skill). 149 | func createSkill( 150 | _ parameter: SkillCreateParameter) 151 | async throws -> SkillResponse 152 | 153 | /// Lists all skills available to your workspace with optional filtering and pagination. 154 | /// 155 | /// - Parameter parameter: Optional parameters for filtering and pagination. 156 | /// 157 | /// - Returns: A [ListSkillsResponse](https://docs.anthropic.com/claude/reference/skills/list-skills). 158 | /// 159 | /// - Throws: An error if the request fails. 160 | /// 161 | /// For more information, refer to [Anthropic's Skills API documentation](https://docs.anthropic.com/claude/reference/skills/list-skills). 162 | func listSkills( 163 | parameter: ListSkillsParameter?) 164 | async throws -> ListSkillsResponse 165 | 166 | /// Retrieves detailed information about a specific skill. 167 | /// 168 | /// - Parameter skillId: The unique identifier of the skill to retrieve. 169 | /// 170 | /// - Returns: A [SkillResponse](https://docs.anthropic.com/claude/reference/skills/get-skill). 171 | /// 172 | /// - Throws: An error if the request fails. 173 | /// 174 | /// For more information, refer to [Anthropic's Skills API documentation](https://docs.anthropic.com/claude/reference/skills/get-skill). 175 | func retrieveSkill( 176 | skillId: String) 177 | async throws -> SkillResponse 178 | 179 | /// Deletes a skill. 180 | /// Note: All versions of the skill must be deleted before the skill itself can be deleted. 181 | /// 182 | /// - Parameter skillId: The unique identifier of the skill to delete. 183 | /// 184 | /// - Throws: An error if the request fails or if versions still exist. 185 | /// 186 | /// For more information, refer to [Anthropic's Skills API documentation](https://docs.anthropic.com/claude/reference/skills/delete-skill). 187 | func deleteSkill( 188 | skillId: String) 189 | async throws 190 | 191 | // MARK: Skill Versions 192 | 193 | /// Creates a new version of an existing skill. 194 | /// 195 | /// - Parameters: 196 | /// - skillId: The unique identifier of the skill. 197 | /// - parameter: Parameters containing the files for the new version. 198 | /// 199 | /// - Returns: A [SkillVersionResponse](https://docs.anthropic.com/claude/reference/skills/create-skill-version). 200 | /// 201 | /// - Throws: An error if the request fails. 202 | /// 203 | /// For more information, refer to [Anthropic's Skills API documentation](https://docs.anthropic.com/claude/reference/skills/create-skill-version). 204 | func createSkillVersion( 205 | skillId: String, 206 | _ parameter: SkillVersionCreateParameter) 207 | async throws -> SkillVersionResponse 208 | 209 | /// Lists all versions of a specific skill with pagination support. 210 | /// 211 | /// - Parameters: 212 | /// - skillId: The unique identifier of the skill. 213 | /// - parameter: Optional parameters for pagination. 214 | /// 215 | /// - Returns: A [ListSkillVersionsResponse](https://docs.anthropic.com/claude/reference/skills/list-skill-versions). 216 | /// 217 | /// - Throws: An error if the request fails. 218 | /// 219 | /// For more information, refer to [Anthropic's Skills API documentation](https://docs.anthropic.com/claude/reference/skills/list-skill-versions). 220 | func listSkillVersions( 221 | skillId: String, 222 | parameter: ListSkillVersionsParameter?) 223 | async throws -> ListSkillVersionsResponse 224 | 225 | /// Retrieves detailed information about a specific skill version. 226 | /// 227 | /// - Parameters: 228 | /// - skillId: The unique identifier of the skill. 229 | /// - version: The version identifier to retrieve. 230 | /// 231 | /// - Returns: A [SkillVersionResponse](https://docs.anthropic.com/claude/reference/skills/get-skill-version). 232 | /// 233 | /// - Throws: An error if the request fails. 234 | /// 235 | /// For more information, refer to [Anthropic's Skills API documentation](https://docs.anthropic.com/claude/reference/skills/get-skill-version). 236 | func retrieveSkillVersion( 237 | skillId: String, 238 | version: String) 239 | async throws -> SkillVersionResponse 240 | 241 | /// Deletes a specific version of a skill. 242 | /// 243 | /// - Parameters: 244 | /// - skillId: The unique identifier of the skill. 245 | /// - version: The version identifier to delete. 246 | /// 247 | /// - Throws: An error if the request fails. 248 | /// 249 | /// For more information, refer to [Anthropic's Skills API documentation](https://docs.anthropic.com/claude/reference/skills/delete-skill-version). 250 | func deleteSkillVersion( 251 | skillId: String, 252 | version: String) 253 | async throws 254 | } 255 | 256 | extension AnthropicService { 257 | 258 | /// Asynchronously fetches a decodable data type from Anthropic's API. 259 | /// 260 | /// - Parameters: 261 | /// - type: The `Decodable` type that the response should be decoded to. 262 | /// - request: The `URLRequest` describing the API request. 263 | /// - debugEnabled: If true the service will print events on DEBUG builds. 264 | /// - Throws: An error if the request fails or if decoding fails. 265 | /// - Returns: A value of the specified decodable type. 266 | public func fetch( 267 | type: T.Type, 268 | with request: URLRequest, 269 | debugEnabled: Bool) 270 | async throws -> T 271 | { 272 | if debugEnabled { 273 | printCurlCommand(request) 274 | } 275 | // Convert URLRequest to HTTPRequest 276 | let httpRequest = try HTTPRequest(from: request) 277 | 278 | let (data, response) = try await httpClient.data(for: httpRequest) 279 | 280 | if debugEnabled { 281 | printHTTPResponse(response, data: data) 282 | } 283 | guard response.statusCode == 200 else { 284 | var errorMessage = "status code \(response.statusCode)" 285 | do { 286 | let errorResponse = try decoder.decode(ErrorResponse.self, from: data) 287 | errorMessage += errorResponse.error.message 288 | } catch { 289 | // If decoding fails, proceed with a general error message 290 | errorMessage = "status code \(response.statusCode)" 291 | } 292 | throw APIError.responseUnsuccessful(description: errorMessage) 293 | } 294 | #if DEBUG 295 | if debugEnabled { 296 | print("DEBUG JSON FETCH API = \(try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any])") 297 | } 298 | #endif 299 | do { 300 | return try decoder.decode(type, from: data) 301 | } catch let DecodingError.keyNotFound(key, context) { 302 | let debug = "Key '\(key.stringValue)' not found: \(context.debugDescription)" 303 | let codingPath = "codingPath: \(context.codingPath)" 304 | let debugMessage = debug + codingPath 305 | #if DEBUG 306 | if debugEnabled { 307 | print(debugMessage) 308 | } 309 | #endif 310 | throw APIError.dataCouldNotBeReadMissingData(description: debugMessage) 311 | } catch { 312 | #if DEBUG 313 | if debugEnabled { 314 | print("\(error)") 315 | } 316 | #endif 317 | throw APIError.jsonDecodingFailure(description: error.localizedDescription) 318 | } 319 | } 320 | 321 | /// Asynchronously fetches a stream of decodable data types from Anthropic's API for chat completions. 322 | /// 323 | /// This method is primarily used for streaming chat completions. 324 | /// 325 | /// - Parameters: 326 | /// - type: The `Decodable` type that each streamed response should be decoded to. 327 | /// - request: The `URLRequest` describing the API request. 328 | /// - debugEnabled: If true the service will print events on DEBUG builds. 329 | /// - Throws: An error if the request fails or if decoding fails. 330 | /// - Returns: An asynchronous throwing stream of the specified decodable type. 331 | public func fetchStream( 332 | type: T.Type, 333 | with request: URLRequest, 334 | debugEnabled: Bool) 335 | async throws -> AsyncThrowingStream 336 | { 337 | if debugEnabled { 338 | printCurlCommand(request) 339 | } 340 | 341 | // Convert URLRequest to HTTPRequest 342 | let httpRequest = try HTTPRequest(from: request) 343 | 344 | let (byteStream, response) = try await httpClient.bytes(for: httpRequest) 345 | 346 | if debugEnabled { 347 | printHTTPResponse(response) 348 | } 349 | guard response.statusCode == 200 else { 350 | var errorMessage = "status code \(response.statusCode)" 351 | // Note: We can't easily collect error data from the stream here 352 | // This is a limitation we accept for now 353 | throw APIError.responseUnsuccessful(description: errorMessage) 354 | } 355 | return AsyncThrowingStream { continuation in 356 | let task = Task { 357 | do { 358 | guard case .lines(let linesStream) = byteStream else { 359 | throw APIError.invalidData 360 | } 361 | for try await line in linesStream { 362 | // TODO: Test the `event` line 363 | if line.hasPrefix("data:"), 364 | let data = line.dropFirst(5).data(using: .utf8) { 365 | #if DEBUG 366 | if debugEnabled { 367 | print("DEBUG JSON STREAM LINE = \(try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any])") 368 | } 369 | #endif 370 | do { 371 | let decoded = try self.decoder.decode(T.self, from: data) 372 | continuation.yield(decoded) 373 | } catch let DecodingError.keyNotFound(key, context) { 374 | let debug = "Key '\(key.stringValue)' not found: \(context.debugDescription)" 375 | let codingPath = "codingPath: \(context.codingPath)" 376 | let debugMessage = debug + codingPath 377 | #if DEBUG 378 | if debugEnabled { 379 | print(debugMessage) 380 | } 381 | #endif 382 | throw APIError.dataCouldNotBeReadMissingData(description: debugMessage) 383 | } catch { 384 | #if DEBUG 385 | if debugEnabled { 386 | debugPrint("CONTINUATION ERROR DECODING \(error.localizedDescription)") 387 | } 388 | #endif 389 | continuation.finish(throwing: error) 390 | } 391 | } 392 | } 393 | continuation.finish() 394 | } catch let DecodingError.keyNotFound(key, context) { 395 | let debug = "Key '\(key.stringValue)' not found: \(context.debugDescription)" 396 | let codingPath = "codingPath: \(context.codingPath)" 397 | let debugMessage = debug + codingPath 398 | #if DEBUG 399 | if debugEnabled { 400 | print(debugMessage) 401 | } 402 | #endif 403 | throw APIError.dataCouldNotBeReadMissingData(description: debugMessage) 404 | } catch { 405 | #if DEBUG 406 | if debugEnabled { 407 | print("CONTINUATION ERROR DECODING \(error.localizedDescription)") 408 | } 409 | #endif 410 | continuation.finish(throwing: error) 411 | } 412 | } 413 | continuation.onTermination = { @Sendable _ in 414 | task.cancel() 415 | } 416 | } 417 | } 418 | 419 | // MARK: Debug Helpers 420 | 421 | private func prettyPrintJSON( 422 | _ data: Data) 423 | -> String? 424 | { 425 | guard 426 | let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []), 427 | let prettyData = try? JSONSerialization.data(withJSONObject: jsonObject, options: [.prettyPrinted]), 428 | let prettyPrintedString = String(data: prettyData, encoding: .utf8) 429 | else { return nil } 430 | return prettyPrintedString 431 | } 432 | 433 | private func printCurlCommand( 434 | _ request: URLRequest) 435 | { 436 | guard let url = request.url, let httpMethod = request.httpMethod else { 437 | debugPrint("Invalid URL or HTTP method.") 438 | return 439 | } 440 | 441 | var baseCommand = "curl \(url.absoluteString)" 442 | 443 | // Add method if not GET 444 | if httpMethod != "GET" { 445 | baseCommand += " -X \(httpMethod)" 446 | } 447 | 448 | // Add headers if any, masking the Authorization token 449 | if let headers = request.allHTTPHeaderFields { 450 | for (header, value) in headers { 451 | let maskedValue = header.lowercased() == "authorization" ? maskAuthorizationToken(value) : value 452 | baseCommand += " \\\n-H \"\(header): \(maskedValue)\"" 453 | } 454 | } 455 | 456 | // Add body if present 457 | if let httpBody = request.httpBody, let bodyString = prettyPrintJSON(httpBody) { 458 | // The body string is already pretty printed and should be enclosed in single quotes 459 | baseCommand += " \\\n-d '\(bodyString)'" 460 | } 461 | 462 | // Print the final command 463 | #if DEBUG 464 | print(baseCommand) 465 | #endif 466 | } 467 | 468 | private func prettyPrintJSON( 469 | _ data: Data) 470 | -> String 471 | { 472 | guard 473 | let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []), 474 | let prettyData = try? JSONSerialization.data(withJSONObject: jsonObject, options: [.prettyPrinted]), 475 | let prettyPrintedString = String(data: prettyData, encoding: .utf8) else { return "Could not print JSON - invalid format" } 476 | return prettyPrintedString 477 | } 478 | 479 | private func printHTTPResponse( 480 | _ response: HTTPResponse, 481 | data: Data? = nil) 482 | { 483 | #if DEBUG 484 | print("\n- - - - - - - - - - INCOMING RESPONSE - - - - - - - - - -\n") 485 | print("Status Code: \(response.statusCode)") 486 | print("Headers: \(response.headers)") 487 | if let data = data, let _ = response.headers["content-type"]?.contains("application/json") { 488 | print("Body: \(prettyPrintJSON(data))") 489 | } else if let data = data, let bodyString = String(data: data, encoding: .utf8) { 490 | print("Body: \(bodyString)") 491 | } 492 | print("\n- - - - - - - - - - - - - - - - - - - - - - - - - - - -\n") 493 | #endif 494 | } 495 | 496 | private func maskAuthorizationToken(_ token: String) -> String { 497 | if token.count > 6 { 498 | let prefix = String(token.prefix(3)) 499 | let suffix = String(token.suffix(3)) 500 | return "\(prefix)................\(suffix)" 501 | } else { 502 | return "INVALID TOKEN LENGTH" 503 | } 504 | } 505 | } 506 | -------------------------------------------------------------------------------- /Sources/Anthropic/Public/ResponseModels/Message/MessageResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageResponse.swift 3 | // 4 | // 5 | // Created by James Rochabrun on 1/28/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// [Message Response](https://docs.anthropic.com/claude/reference/messages_post) 11 | public struct MessageResponse: Decodable { 12 | /// Unique object identifier. 13 | /// 14 | /// The format and length of IDs may change over time. 15 | public let id: String? 16 | 17 | /// e.g: "message" 18 | public let type: String? 19 | 20 | /// The model that handled the request. 21 | public let model: String? 22 | 23 | /// Conversational role of the generated message. 24 | /// 25 | /// This will always be "assistant". 26 | public let role: String 27 | 28 | /// Array of Content objects representing blocks of content generated by the model. 29 | /// 30 | /// Each content block has a `type` that determines its structure. 31 | /// 32 | /// - Example text: 33 | /// ``` 34 | /// [{"type": "text", "text": "Hi, I'm Claude."}] 35 | /// ``` 36 | /// 37 | /// - Example thinking: 38 | /// ``` 39 | /// [{"type": "thinking", "thinking": "To approach this, let's think about...", "signature": "zbbJhb..."}] 40 | /// ``` 41 | /// 42 | /// - Example tool use: 43 | /// ``` 44 | /// [{"type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "get_weather", "input": { "location": "San Francisco, CA", "unit": "celsius"}}] 45 | /// ``` 46 | /// This structure facilitates the integration and manipulation of model-generated content within your application. 47 | public let content: [Content] 48 | 49 | /// indicates why the process was halted. 50 | /// 51 | /// This property can hold one of the following values to describe the stop reason: 52 | /// - `"end_turn"`: The model reached a natural stopping point. 53 | /// - `"max_tokens"`: The requested `max_tokens` limit or the model's maximum token limit was exceeded. 54 | /// - `"stop_sequence"`: A custom stop sequence provided by you was generated. 55 | /// 56 | /// It's important to note that the values for `stopReason` here differ from those in `/v1/complete`, specifically in how `end_turn` and `stop_sequence` are distinguished. 57 | /// 58 | /// - In non-streaming mode, `stopReason` is always non-null, indicating the reason for stopping. 59 | /// - In streaming mode, `stopReason` is null in the `message_start` event and non-null in all other cases, providing context for the stoppage. 60 | /// 61 | /// This design allows for a detailed understanding of the process flow and its termination points. 62 | public let stopReason: String? 63 | 64 | /// Which custom stop sequence was generated. 65 | /// 66 | /// This value will be non-null if one of your custom stop sequences was generated. 67 | public let stopSequence: String? 68 | 69 | /// Container for the number of tokens used. 70 | public let usage: Usage 71 | 72 | /// Container information when skills are used. 73 | /// Contains the container ID that can be reused in subsequent requests. 74 | public let container: ContainerInfo? 75 | 76 | /// Container information returned in responses when skills are used 77 | public struct ContainerInfo: Decodable { 78 | /// The container ID that can be reused across multiple messages 79 | public let id: String? 80 | } 81 | 82 | public enum Content: Codable { 83 | public typealias Input = [String: DynamicContent] 84 | public typealias Citations = [Citation] 85 | 86 | public struct ToolUse: Codable { 87 | public let id: String 88 | public let name: String 89 | public let input: Input 90 | } 91 | 92 | public struct Thinking: Codable { 93 | public let thinking: String 94 | public let signature: String? 95 | } 96 | 97 | public struct ServerToolUse: Codable { 98 | public let id: String 99 | public let input: Input 100 | public let type: String 101 | public let name: String 102 | } 103 | 104 | public struct ToolResult: Codable { 105 | public let content: ToolResultContent 106 | public let isError: Bool? 107 | public let toolUseId: String? 108 | } 109 | 110 | public struct WebSearchToolResult: Codable { 111 | public let toolUseId: String? 112 | public let content: [ContentItem] 113 | public let type: String 114 | 115 | private enum CodingKeys: String, CodingKey { 116 | case toolUseId = "tool_use_id" 117 | case content 118 | case type 119 | } 120 | } 121 | 122 | /// Generic code execution tool result for Skills API 123 | /// Handles text_editor_code_execution_tool_result, bash_code_execution_tool_result, etc. 124 | public struct CodeExecutionToolResult: Codable { 125 | public let type: String 126 | public let toolUseId: String? 127 | public let content: DynamicContent 128 | 129 | private enum CodingKeys: String, CodingKey { 130 | case type 131 | case toolUseId = "tool_use_id" 132 | case content 133 | } 134 | } 135 | 136 | case text(String, Citations?) 137 | case toolUse(ToolUse) 138 | case thinking(Thinking) 139 | case serverToolUse(ServerToolUse) 140 | case webSearchToolResult(WebSearchToolResult) 141 | case toolResult(ToolResult) 142 | case codeExecutionToolResult(CodeExecutionToolResult) 143 | 144 | private enum CodingKeys: String, CodingKey { 145 | case type, text, id, name, input, citations, thinking, signature 146 | case toolUseId = "tool_use_id" 147 | case content 148 | case isError 149 | } 150 | 151 | public enum DynamicContent: Codable { 152 | case string(String) 153 | case integer(Int) 154 | case double(Double) 155 | case dictionary(Input) 156 | case array([DynamicContent]) 157 | case bool(Bool) 158 | case null 159 | 160 | public init(from decoder: Decoder) throws { 161 | let container = try decoder.singleValueContainer() 162 | if let intValue = try? container.decode(Int.self) { 163 | self = .integer(intValue) 164 | } else if let doubleValue = try? container.decode(Double.self) { 165 | self = .double(doubleValue) 166 | } else if let stringValue = try? container.decode(String.self) { 167 | self = .string(stringValue) 168 | } else if let boolValue = try? container.decode(Bool.self) { 169 | self = .bool(boolValue) 170 | } else if container.decodeNil() { 171 | self = .null 172 | } else if let arrayValue = try? container.decode([DynamicContent].self) { 173 | self = .array(arrayValue) 174 | } else if let dictionaryValue = try? container.decode([String: DynamicContent].self) { 175 | self = .dictionary(dictionaryValue) 176 | } else { 177 | throw DecodingError.dataCorruptedError(in: container, debugDescription: "Content cannot be decoded") 178 | } 179 | } 180 | 181 | public func encode(to encoder: any Encoder) throws { 182 | var container = encoder.singleValueContainer() 183 | switch self { 184 | case .string(let val): 185 | try container.encode(val) 186 | case .integer(let val): 187 | try container.encode(val) 188 | case .double(let val): 189 | try container.encode(val) 190 | case .dictionary(let val): 191 | try container.encode(val) 192 | case .array(let val): 193 | try container.encode(val) 194 | case .bool(let val): 195 | try container.encode(val) 196 | case .null: 197 | try container.encodeNil() 198 | } 199 | } 200 | } 201 | 202 | public init(from decoder: Decoder) throws { 203 | let container = try decoder.container(keyedBy: CodingKeys.self) 204 | let type = try container.decode(String.self, forKey: .type) 205 | switch type { 206 | case "text": 207 | let text = try container.decode(String.self, forKey: .text) 208 | let citations = try container.decodeIfPresent(Citations.self, forKey: .citations) 209 | self = .text(text, citations) 210 | case "tool_use": 211 | let id = try container.decode(String.self, forKey: .id) 212 | let name = try container.decode(String.self, forKey: .name) 213 | let input = try container.decode(Input.self, forKey: .input) 214 | self = .toolUse(ToolUse(id: id, name: name, input: input)) 215 | case "thinking": 216 | let thinking = try container.decode(String.self, forKey: .thinking) 217 | let signature = try container.decodeIfPresent(String.self, forKey: .signature) 218 | self = .thinking(Thinking(thinking: thinking, signature: signature)) 219 | case "server_tool_use": 220 | let id = try container.decode(String.self, forKey: .id) 221 | let name = try container.decode(String.self, forKey: .name) 222 | let input = try container.decode(Input.self, forKey: .input) 223 | let type = try container.decode(String.self, forKey: .type) 224 | self = .serverToolUse(ServerToolUse(id: id, input: input, type: type, name: name)) 225 | case "web_search_tool_result": 226 | let toolUseId = try container.decodeIfPresent(String.self, forKey: .toolUseId) 227 | let content = try container.decode([ContentItem].self, forKey: .content) 228 | let type = try container.decode(String.self, forKey: .type) 229 | self = .webSearchToolResult(WebSearchToolResult(toolUseId: toolUseId, content: content, type: type)) 230 | case "tool_result": 231 | let toolUseId = try container.decodeIfPresent(String.self, forKey: .toolUseId) 232 | let isError = try container.decodeIfPresent(Bool.self, forKey: .isError) ?? false 233 | 234 | // Now decode the flexible content type 235 | let content = try container.decode(ToolResultContent.self, forKey: .content) 236 | self = .toolResult(ToolResult(content: content, isError: isError, toolUseId: toolUseId)) 237 | default: 238 | // Handle code execution tool results (text_editor, bash, etc.) 239 | if type.hasSuffix("_tool_result") { 240 | let toolUseId = try container.decodeIfPresent(String.self, forKey: .toolUseId) 241 | let content = try container.decode(DynamicContent.self, forKey: .content) 242 | self = .codeExecutionToolResult(CodeExecutionToolResult(type: type, toolUseId: toolUseId, content: content)) 243 | } else { 244 | throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Invalid type value found in JSON: \(type)") 245 | } 246 | } 247 | } 248 | 249 | public func encode(to encoder: any Encoder) throws { 250 | var container = encoder.container(keyedBy: CodingKeys.self) 251 | switch self { 252 | case .text(let text, let citations): 253 | try container.encode("text", forKey: .type) 254 | try container.encode(text, forKey: .text) 255 | try container.encodeIfPresent(citations, forKey: .citations) 256 | case .toolUse(let toolUse): 257 | try container.encode("tool_use", forKey: .type) 258 | try container.encode(toolUse.id, forKey: .id) 259 | try container.encode(toolUse.name, forKey: .name) 260 | try container.encode(toolUse.input, forKey: .input) 261 | case .thinking(let thinking): 262 | try container.encode("thinking", forKey: .type) 263 | try container.encode(thinking.thinking, forKey: .thinking) 264 | try container.encodeIfPresent(thinking.signature, forKey: .signature) 265 | case .serverToolUse(let serverToolUse): 266 | try container.encode("server_tool_use", forKey: .type) 267 | try container.encode(serverToolUse.id, forKey: .id) 268 | try container.encode(serverToolUse.name, forKey: .name) 269 | try container.encode(serverToolUse.input, forKey: .input) 270 | case .webSearchToolResult(let webSearchResult): 271 | try container.encode("web_search_tool_result", forKey: .type) 272 | try container.encode(webSearchResult.toolUseId, forKey: .toolUseId) 273 | try container.encode(webSearchResult.content, forKey: .content) 274 | case .toolResult(let toolResult): 275 | try container.encode("tool_result", forKey: .type) 276 | try container.encode(toolResult.content, forKey: .content) 277 | try container.encodeIfPresent(toolResult.isError, forKey: .isError) 278 | try container.encodeIfPresent(toolResult.toolUseId, forKey: .toolUseId) 279 | case .codeExecutionToolResult(let codeExecResult): 280 | try container.encode(codeExecResult.type, forKey: .type) 281 | try container.encodeIfPresent(codeExecResult.toolUseId, forKey: .toolUseId) 282 | try container.encode(codeExecResult.content, forKey: .content) 283 | } 284 | } 285 | } 286 | 287 | /// Claude is capable of providing detailed citations when answering questions about documents, helping you track and verify information sources in responses. 288 | /// https://docs.anthropic.com/en/docs/build-with-claude/citations 289 | public enum Citation: Codable { 290 | case charLocation(CharLocation) 291 | case pageLocation(PageLocation) 292 | case contentBlockLocation(ContentBlockLocation) 293 | case webSearchResultLocation(WebSearchResultLocation) 294 | 295 | private enum CodingKeys: String, CodingKey { 296 | case type 297 | case citedText 298 | case documentIndex 299 | case documentTitle 300 | case startCharIndex 301 | case endCharIndex 302 | case startPageNumber 303 | case endPageNumber 304 | case startBlockIndex 305 | case endBlockIndex 306 | case url 307 | case title 308 | case encryptedIndex 309 | } 310 | 311 | public struct CharLocation: Codable { 312 | public let citedText: String? 313 | public let documentIndex: Int? 314 | public let documentTitle: String? 315 | public let startCharIndex: Int? 316 | public let endCharIndex: Int? 317 | } 318 | 319 | public struct PageLocation: Codable { 320 | public let citedText: String? 321 | public let documentIndex: Int? 322 | public let documentTitle: String? 323 | public let startPageNumber: Int? 324 | public let endPageNumber: Int? 325 | } 326 | 327 | public struct ContentBlockLocation: Codable { 328 | public let citedText: String? 329 | public let documentIndex: Int? 330 | public let documentTitle: String? 331 | public let startBlockIndex: Int? 332 | public let endBlockIndex: Int? 333 | } 334 | 335 | public struct WebSearchResultLocation: Codable { 336 | public let url: String? 337 | public let title: String? 338 | public let encryptedIndex: String? 339 | public let citedText: String? 340 | } 341 | 342 | public init(from decoder: Decoder) throws { 343 | let container = try decoder.container(keyedBy: CodingKeys.self) 344 | let type = try container.decode(String.self, forKey: .type) 345 | 346 | switch type { 347 | case "char_location": 348 | self = .charLocation(CharLocation( 349 | citedText: try container.decodeIfPresent(String.self, forKey: .citedText), 350 | documentIndex: try container.decodeIfPresent(Int.self, forKey: .documentIndex), 351 | documentTitle: try container.decodeIfPresent(String.self, forKey: .documentTitle), 352 | startCharIndex: try container.decodeIfPresent(Int.self, forKey: .startCharIndex), 353 | endCharIndex: try container.decodeIfPresent(Int.self, forKey: .endCharIndex) 354 | )) 355 | case "page_location": 356 | self = .pageLocation(PageLocation( 357 | citedText: try container.decodeIfPresent(String.self, forKey: .citedText), 358 | documentIndex: try container.decodeIfPresent(Int.self, forKey: .documentIndex), 359 | documentTitle: try container.decodeIfPresent(String.self, forKey: .documentTitle), 360 | startPageNumber: try container.decodeIfPresent(Int.self, forKey: .startPageNumber), 361 | endPageNumber: try container.decodeIfPresent(Int.self, forKey: .endPageNumber) 362 | )) 363 | case "content_block_location": 364 | self = .contentBlockLocation(ContentBlockLocation( 365 | citedText: try container.decodeIfPresent(String.self, forKey: .citedText), 366 | documentIndex: try container.decodeIfPresent(Int.self, forKey: .documentIndex), 367 | documentTitle: try container.decodeIfPresent(String.self, forKey: .documentTitle), 368 | startBlockIndex: try container.decodeIfPresent(Int.self, forKey: .startBlockIndex), 369 | endBlockIndex: try container.decodeIfPresent(Int.self, forKey: .endBlockIndex) 370 | )) 371 | case "web_search_result_location": 372 | self = .webSearchResultLocation(WebSearchResultLocation( 373 | url: try container.decodeIfPresent(String.self, forKey: .url), 374 | title: try container.decodeIfPresent(String.self, forKey: .title), 375 | encryptedIndex: try container.decodeIfPresent(String.self, forKey: .encryptedIndex), 376 | citedText: try container.decodeIfPresent(String.self, forKey: .citedText) 377 | )) 378 | default: 379 | throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Invalid citation type!") 380 | } 381 | } 382 | 383 | public func encode(to encoder: Encoder) throws { 384 | var container = encoder.container(keyedBy: CodingKeys.self) 385 | 386 | switch self { 387 | case .charLocation(let location): 388 | try container.encode("char_location", forKey: .type) 389 | try container.encodeIfPresent(location.citedText, forKey: .citedText) 390 | try container.encodeIfPresent(location.documentIndex, forKey: .documentIndex) 391 | try container.encodeIfPresent(location.documentTitle, forKey: .documentTitle) 392 | try container.encodeIfPresent(location.startCharIndex, forKey: .startCharIndex) 393 | try container.encodeIfPresent(location.endCharIndex, forKey: .endCharIndex) 394 | case .pageLocation(let location): 395 | try container.encode("page_location", forKey: .type) 396 | try container.encodeIfPresent(location.citedText, forKey: .citedText) 397 | try container.encodeIfPresent(location.documentIndex, forKey: .documentIndex) 398 | try container.encodeIfPresent(location.documentTitle, forKey: .documentTitle) 399 | try container.encodeIfPresent(location.startPageNumber, forKey: .startPageNumber) 400 | try container.encodeIfPresent(location.endPageNumber, forKey: .endPageNumber) 401 | case .contentBlockLocation(let location): 402 | try container.encode("content_block_location", forKey: .type) 403 | try container.encodeIfPresent(location.citedText, forKey: .citedText) 404 | try container.encodeIfPresent(location.documentIndex, forKey: .documentIndex) 405 | try container.encodeIfPresent(location.documentTitle, forKey: .documentTitle) 406 | try container.encodeIfPresent(location.startBlockIndex, forKey: .startBlockIndex) 407 | try container.encodeIfPresent(location.endBlockIndex, forKey: .endBlockIndex) 408 | case .webSearchResultLocation(let location): 409 | try container.encode("web_search_result_location", forKey: .type) 410 | try container.encodeIfPresent(location.url, forKey: .url) 411 | try container.encodeIfPresent(location.title, forKey: .title) 412 | try container.encodeIfPresent(location.encryptedIndex, forKey: .encryptedIndex) 413 | try container.encodeIfPresent(location.citedText, forKey: .citedText) 414 | } 415 | } 416 | } 417 | 418 | public struct Usage: Codable { 419 | /// The number of input tokens which were used. 420 | public let inputTokens: Int? 421 | 422 | /// The number of output tokens which were used. 423 | public let outputTokens: Int 424 | 425 | /// The number of thinking tokens which were used (when thinking mode is enabled). 426 | public let thinkingTokens: Int? 427 | 428 | /// [Prompt Caching](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#how-can-i-track-the-effectiveness-of-my-caching-strategy) 429 | /// You can monitor cache performance using the cache_creation_input_tokens and cache_read_input_tokens fields in the API response. 430 | public let cacheCreationInputTokens: Int? 431 | 432 | /// [Prompt Caching](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#how-can-i-track-the-effectiveness-of-my-caching-strategy) 433 | /// You can monitor cache performance using the cache_creation_input_tokens and cache_read_input_tokens fields in the API response. 434 | public let cacheReadInputTokens: Int? 435 | 436 | /// Server tool usage information - NEW 437 | public let serverToolUse: ServerToolUse? 438 | } 439 | 440 | public struct ServerToolUse: Codable { 441 | /// Number of web search requests performed 442 | public let webSearchRequests: Int? 443 | } 444 | } 445 | 446 | /// Extension to provide convenient access to thinking content 447 | extension MessageResponse { 448 | 449 | /// Extracts all thinking content blocks from the response 450 | /// - Returns: Array of thinking content or empty array if none found 451 | public func getThinkingContent() -> [Content.Thinking] { 452 | return content.compactMap { contentBlock in 453 | if case .thinking(let thinking) = contentBlock { 454 | return thinking 455 | } 456 | return nil 457 | } 458 | } 459 | 460 | /// Get the first thinking content block from the response 461 | /// - Returns: The first thinking content block or nil if none exists 462 | public func getFirstThinkingContent() -> Content.Thinking? { 463 | return getThinkingContent().first 464 | } 465 | 466 | /// Get the combined thinking content as a single string 467 | /// - Returns: All thinking content concatenated into a single string, or nil if no thinking content exists 468 | public func getCombinedThinkingContent() -> String? { 469 | let thinkingBlocks = getThinkingContent() 470 | if thinkingBlocks.isEmpty { 471 | return nil 472 | } 473 | 474 | return thinkingBlocks.map { $0.thinking }.joined(separator: "\n\n") 475 | } 476 | 477 | /// Determines if the response contains any thinking content 478 | /// - Returns: True if thinking content exists, false otherwise 479 | public var hasThinkingContent: Bool { 480 | return content.contains { contentBlock in 481 | if case .thinking = contentBlock { 482 | return true 483 | } 484 | return false 485 | } 486 | } 487 | } 488 | 489 | // MARK: MessageResponse.Content + DynamicContent 490 | 491 | public extension MessageResponse.Content.DynamicContent { 492 | var stringValue: String? { 493 | if case .string(let value) = self { 494 | return value 495 | } 496 | return nil 497 | } 498 | 499 | var intValue: Int? { 500 | if case .integer(let value) = self { 501 | return value 502 | } 503 | return nil 504 | } 505 | 506 | var boolValue: Bool? { 507 | if case .bool(let value) = self { 508 | return value 509 | } 510 | return nil 511 | } 512 | 513 | var arrayValue: [MessageResponse.Content.DynamicContent]? { 514 | if case .array(let value) = self { 515 | return value 516 | } 517 | return nil 518 | } 519 | 520 | var dictionaryValue: [String: MessageResponse.Content.DynamicContent]? { 521 | if case .dictionary(let value) = self { 522 | return value 523 | } 524 | return nil 525 | } 526 | } 527 | 528 | // MARK: MessageResponse.Content + TextEditorCommand 529 | 530 | public extension MessageResponse.Content { 531 | 532 | enum TextEditorCommand: String { 533 | case view 534 | case str_replace 535 | case insert 536 | case create 537 | case undo_edit 538 | 539 | // Helper to extract command from input 540 | public static func from(_ input: [String: DynamicContent]) -> TextEditorCommand? { 541 | guard let commandValue = input["command"]?.stringValue else { return nil } 542 | return TextEditorCommand(rawValue: commandValue) 543 | } 544 | } 545 | } 546 | 547 | // MARK: MessageResponse.Content + ToolResultContent 548 | 549 | public extension MessageResponse.Content { 550 | 551 | public enum ToolResultContent: Codable { 552 | case string(String) 553 | case items([ContentItem]) 554 | 555 | public init(from decoder: Decoder) throws { 556 | let container = try decoder.singleValueContainer() 557 | 558 | if let stringValue = try? container.decode(String.self) { 559 | self = .string(stringValue) 560 | } else if let itemsArray = try? container.decode([ContentItem].self) { 561 | self = .items(itemsArray) 562 | } else { 563 | throw DecodingError.dataCorruptedError( 564 | in: container, 565 | debugDescription: "ToolResultContent must be either String or [ContentItem]" 566 | ) 567 | } 568 | } 569 | 570 | public func encode(to encoder: Encoder) throws { 571 | var container = encoder.singleValueContainer() 572 | 573 | switch self { 574 | case .string(let value): 575 | try container.encode(value) 576 | case .items(let items): 577 | try container.encode(items) 578 | } 579 | } 580 | } 581 | } 582 | 583 | // MARK: ContentItem 584 | 585 | public struct ContentItem: Codable { 586 | public let encryptedContent: String? 587 | public let title: String? 588 | public let pageAge: String? 589 | public let type: String? 590 | public let url: String? 591 | public let text: String? 592 | 593 | var description: String { 594 | var result = "ContentItem:\n" 595 | 596 | if let title = self.title { 597 | result += " Title: \"\(title)\"\n" 598 | } 599 | 600 | if let url = self.url { 601 | result += " URL: \(url)\n" 602 | } 603 | 604 | if let type = self.type { 605 | result += " Type: \(type)\n" 606 | } 607 | 608 | if let pageAge = self.pageAge { 609 | result += " Age: \(pageAge)\n" 610 | } 611 | 612 | if let text = self.text { 613 | // Limit text length for readability 614 | let truncatedText = text.count > 100 ? "\(text.prefix(100))..." : text 615 | result += " Text: \"\(truncatedText)\"\n" 616 | } 617 | 618 | if let encryptedContent = self.encryptedContent { 619 | // Just indicate presence rather than showing the whole encrypted content 620 | result += " Encrypted Content: [Present]\n" 621 | } 622 | 623 | return result 624 | } 625 | 626 | private enum CodingKeys: String, CodingKey { 627 | case encryptedContent = "encrypted_content" 628 | case title 629 | case text 630 | case pageAge = "page_age" 631 | case type 632 | case url 633 | } 634 | } 635 | --------------------------------------------------------------------------------