├── 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 |
--------------------------------------------------------------------------------