@@ -18,7 +18,7 @@ final class AnthropicProvider: AIProvider {
1818
1919 init ( endpoint: String , apiKey: String ) {
2020 self . endpoint = endpoint. hasSuffix ( " / " ) ? String ( endpoint. dropLast ( ) ) : endpoint
21- self . apiKey = apiKey
21+ self . apiKey = apiKey. trimmingCharacters ( in : . whitespacesAndNewlines )
2222 self . session = URLSession ( configuration: . ephemeral)
2323 }
2424
@@ -94,32 +94,66 @@ final class AnthropicProvider: AIProvider {
9494 }
9595
9696 func fetchAvailableModels( ) async throws -> [ String ] {
97- // Anthropic doesn't have a models endpoint; return known models
98- [
99- " claude-sonnet-4-5-20250514 " ,
100- " claude-haiku-4-5-20251001 " ,
101- " claude-opus-4-20250514 "
102- ]
97+ guard let url = URL ( string: " \( endpoint) /v1/models " ) else {
98+ throw AIProviderError . invalidEndpoint ( endpoint)
99+ }
100+
101+ var request = URLRequest ( url: url)
102+ request. httpMethod = " GET "
103+ request. setValue ( apiKey, forHTTPHeaderField: " x-api-key " )
104+ request. setValue ( " 2023-06-01 " , forHTTPHeaderField: " anthropic-version " )
105+
106+ let ( data, response) = try await session. data ( for: request)
107+
108+ guard let httpResponse = response as? HTTPURLResponse ,
109+ httpResponse. statusCode == 200 ,
110+ let json = try ? JSONSerialization . jsonObject ( with: data) as? [ String : Any ] ,
111+ let models = json [ " data " ] as? [ [ String : Any ] ]
112+ else {
113+ return Self . knownModels
114+ }
115+
116+ let modelIds = models. compactMap { $0 [ " id " ] as? String }
117+ return modelIds. isEmpty ? Self . knownModels : modelIds
103118 }
104119
120+ private static let knownModels = [
121+ " claude-sonnet-4-6 " ,
122+ " claude-opus-4-6 " ,
123+ " claude-haiku-4-5-20251001 " ,
124+ " claude-sonnet-4-5-20250929 " ,
125+ " claude-opus-4-5-20251101 "
126+ ]
127+
105128 func testConnection( ) async throws -> Bool {
106129 let testMessage = AIChatMessage ( role: . user, content: " Hi " )
107130 let request = try buildMessagesRequest (
108131 messages: [ testMessage] ,
109- model: " claude-sonnet -4-5-20250514 " ,
132+ model: " claude-haiku -4-5-20251001 " ,
110133 systemPrompt: nil ,
111134 maxTokens: 1 ,
112135 stream: false
113136 )
114137
115- let ( _ , response) = try await session. data ( for: request)
138+ let ( data , response) = try await session. data ( for: request)
116139
117140 guard let httpResponse = response as? HTTPURLResponse else {
118141 return false
119142 }
120143
121- // 200 = success, 401 = bad key
122- return httpResponse. statusCode == 200
144+ let statusCode = httpResponse. statusCode
145+
146+ // 200 = full success, 400 = key is valid but request was rejected (e.g. billing)
147+ if statusCode == 200 || statusCode == 400 {
148+ return true
149+ }
150+
151+ if statusCode == 401 {
152+ throw AIProviderError . authenticationFailed ( " " )
153+ }
154+
155+ let body = String ( data: data, encoding: . utf8) ?? " "
156+ throw mapHTTPError ( statusCode: statusCode, body: body)
123157 }
124158
125159 // MARK: - Private
@@ -209,21 +243,25 @@ final class AnthropicProvider: AIProvider {
209243 var body = " "
210244 for try await line in bytes. lines {
211245 body += line
212- if body. count > 2_000 { break }
246+ if ( body as NSString ) . length > 2_000 { break }
213247 }
214248 return body
215249 }
216250
217251 private func mapHTTPError( statusCode: Int , body: String ) -> AIProviderError {
252+ let message = AIProviderError . parseErrorMessage ( from: body) ?? body
253+
218254 switch statusCode {
255+ case 400 :
256+ return . serverError( statusCode, message)
219257 case 401 :
220- return . authenticationFailed( body )
258+ return . authenticationFailed( message )
221259 case 429 :
222260 return . rateLimited
223261 case 404 :
224- return . modelNotFound( body )
262+ return . modelNotFound( message )
225263 default :
226- return . serverError( statusCode, body )
264+ return . serverError( statusCode, message )
227265 }
228266 }
229267}
0 commit comments