Skip to content
This repository was archived by the owner on May 23, 2026. It is now read-only.

Commit decb3f5

Browse files
committed
Add rate limiting error handling and update validation error types in FoundryKit
1 parent 3a77cd7 commit decb3f5

10 files changed

Lines changed: 258104 additions & 23 deletions

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
"Bash(./find_unused_functions.sh)",
2929
"Bash(#!/bin/bash\n# Search for schema types\necho \"=== SEARCHING FOR SCHEMA TYPE USAGE ===\"\n\nsearch_type() {\n local type_name=\"$1\"\n echo \"\"\n echo \"Searching for: $type_name\"\n rg -c \"\\b$type_name\\b\" --type swift ./Sources ./Tests ./Examples 2>/dev/null | grep -v \":0$\" | sort\n}\n\n# Types from GenerationSchema.swift\nsearch_type \"SchemaType\"\nsearch_type \"RuntimeGenerationSchema\"\nsearch_type \"SchemaNode\"\nsearch_type \"Constraint\"\nsearch_type \"DynamicGenerationSchema\")",
3030
"Bash(swift package:*)",
31-
"Bash(gh release edit:*)"
31+
"Bash(gh release edit:*)",
32+
"WebFetch(domain:github.com)"
3233
],
3334
"deny": [],
3435
"includeCoAuthoredBy": false

Sources/FoundryKit/FoundationBackend.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,13 @@ internal final class FoundationBackend: FoundryBackend {
217217
underlyingErrors: context.underlyingErrors
218218
)
219219
)
220+
case .rateLimited(let context):
221+
return .rateLimited(
222+
FoundryGenerationError.Context(
223+
debugDescription: context.debugDescription,
224+
underlyingErrors: context.underlyingErrors
225+
)
226+
)
220227
@unknown default:
221228
return .unknown(
222229
FoundryGenerationError.Context(

Sources/FoundryKit/FoundryErrors.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ public enum FoundryGenerationError: Error, LocalizedError {
4848
/// The requested feature is not supported.
4949
case unsupportedFeature(Context)
5050

51+
/// The request was rate limited.
52+
case rateLimited(Context)
53+
5154
/// An unknown error occurred during generation.
5255
case unknown(Context)
5356

@@ -73,6 +76,8 @@ public enum FoundryGenerationError: Error, LocalizedError {
7376
return "A network error occurred."
7477
case .unsupportedFeature:
7578
return "The requested feature is not supported."
79+
case .rateLimited:
80+
return "The request was rate limited. Please try again later."
7681
case .unknown:
7782
return "An unknown error occurred."
7883
}
@@ -100,6 +105,8 @@ public enum FoundryGenerationError: Error, LocalizedError {
100105
return "Check your internet connection and try again."
101106
case .unsupportedFeature:
102107
return "Use an alternative approach or check the documentation for supported features."
108+
case .rateLimited:
109+
return "Wait a moment before retrying or reduce the frequency of your requests."
103110
case .unknown:
104111
return "Try again or contact support if the problem persists."
105112
}
@@ -117,6 +124,7 @@ public enum FoundryGenerationError: Error, LocalizedError {
117124
.backendUnavailable(let context),
118125
.networkError(let context),
119126
.unsupportedFeature(let context),
127+
.rateLimited(let context),
120128
.unknown(let context):
121129
return context.debugDescription
122130
}

Sources/FoundryKit/FoundryModelSession.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ extension FoundryModelSession {
255255
let response = try await session.respond(to: prompt, schema: generationSchema)
256256

257257
// Convert to our response type
258-
let transcriptEntries = session.transcript.entries.suffix(2)
258+
let transcriptEntries = Array(session.transcript).suffix(2)
259259
updateTranscript(with: transcriptEntries)
260260
return Response(content: response.content, transcriptEntries: transcriptEntries)
261261

@@ -285,7 +285,7 @@ extension FoundryModelSession {
285285
let session = LanguageModelSession(model: .default, guardrails: guardrails, tools: tools)
286286
let response = try await session.respond(to: prompt, schema: generationSchema)
287287

288-
let transcriptEntries = session.transcript.entries.suffix(2)
288+
let transcriptEntries = Array(session.transcript).suffix(2)
289289
updateTranscript(with: transcriptEntries)
290290
return Response(content: response.content, transcriptEntries: transcriptEntries)
291291

@@ -461,9 +461,9 @@ extension FoundryModelSession {
461461

462462
private func updateTranscript(with entries: ArraySlice<Transcript.Entry>) {
463463
// Update internal transcript with new entries
464-
for entry in entries {
465-
transcript.entries.append(entry)
466-
}
464+
// Note: In beta 2, Transcript.init(entries:) is private
465+
// TODO: Find alternative approach for updating transcript
466+
// For now, we'll rely on the session managing its own transcript
467467
}
468468

469469
// Commented out for 0.0.1 release - focusing on simple text generation only

Sources/FoundryKit/Macros/FoundryGenerableMacros.swift

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -189,83 +189,83 @@ extension FoundryStructuredOutput {
189189
switch constraint {
190190
case .minimum(let minValue):
191191
if let intValue = value as? Int, intValue < minValue {
192-
throw ValidationError.belowMinimum(name, minValue)
192+
throw MacroValidationError.belowMinimum(name, minValue)
193193
}
194194
case .maximum(let maxValue):
195195
if let intValue = value as? Int, intValue > maxValue {
196-
throw ValidationError.aboveMaximum(name, maxValue)
196+
throw MacroValidationError.aboveMaximum(name, maxValue)
197197
}
198198
case .range(let range):
199199
if let intValue = value as? Int, !range.contains(intValue) {
200-
throw ValidationError.outOfRange(name, range)
200+
throw MacroValidationError.outOfRange(name, range)
201201
}
202202
case .minimumFloat(let minValue):
203203
if let floatValue = value as? Float, floatValue < minValue {
204-
throw ValidationError.belowMinimumFloat(name, minValue)
204+
throw MacroValidationError.belowMinimumFloat(name, minValue)
205205
}
206206
case .maximumFloat(let maxValue):
207207
if let floatValue = value as? Float, floatValue > maxValue {
208-
throw ValidationError.aboveMaximumFloat(name, maxValue)
208+
throw MacroValidationError.aboveMaximumFloat(name, maxValue)
209209
}
210210
case .rangeFloat(let range):
211211
if let floatValue = value as? Float, !range.contains(floatValue) {
212-
throw ValidationError.outOfRangeFloat(name, range)
212+
throw MacroValidationError.outOfRangeFloat(name, range)
213213
}
214214
case .minimumDouble(let minValue):
215215
if let doubleValue = value as? Double, doubleValue < minValue {
216-
throw ValidationError.belowMinimumDouble(name, minValue)
216+
throw MacroValidationError.belowMinimumDouble(name, minValue)
217217
}
218218
case .maximumDouble(let maxValue):
219219
if let doubleValue = value as? Double, doubleValue > maxValue {
220-
throw ValidationError.aboveMaximumDouble(name, maxValue)
220+
throw MacroValidationError.aboveMaximumDouble(name, maxValue)
221221
}
222222
case .rangeDouble(let range):
223223
if let doubleValue = value as? Double, !range.contains(doubleValue) {
224-
throw ValidationError.outOfRangeDouble(name, range)
224+
throw MacroValidationError.outOfRangeDouble(name, range)
225225
}
226226
case .minimumCount(let minCount):
227227
if let arrayValue = value as? [Any], arrayValue.count < minCount {
228-
throw ValidationError.tooFewItems(name, minCount)
228+
throw MacroValidationError.tooFewItems(name, minCount)
229229
}
230230
case .maximumCount(let maxCount):
231231
if let arrayValue = value as? [Any], arrayValue.count > maxCount {
232-
throw ValidationError.tooManyItems(name, maxCount)
232+
throw MacroValidationError.tooManyItems(name, maxCount)
233233
}
234234
case .count(let constraint):
235235
if let arrayValue = value as? [Any] {
236236
switch constraint {
237237
case .exact(let exactCount):
238238
if arrayValue.count != exactCount {
239-
throw ValidationError.wrongItemCount(name, exactCount, arrayValue.count)
239+
throw MacroValidationError.wrongItemCount(name, exactCount, arrayValue.count)
240240
}
241241
case .range(let range):
242242
if !range.contains(arrayValue.count) {
243-
throw ValidationError.itemCountOutOfRange(name, range, arrayValue.count)
243+
throw MacroValidationError.itemCountOutOfRange(name, range, arrayValue.count)
244244
}
245245
}
246246
}
247247
case .pattern(let regex):
248248
if let stringValue = value as? String {
249249
let predicate = NSPredicate(format: "SELF MATCHES %@", regex)
250250
if !predicate.evaluate(with: stringValue) {
251-
throw ValidationError.patternMismatch(name, regex)
251+
throw MacroValidationError.patternMismatch(name, regex)
252252
}
253253
}
254254
case .anyOf(let allowed):
255255
if let stringValue = value as? String, !allowed.contains(stringValue) {
256-
throw ValidationError.invalidEnumValue(name, stringValue, allowed)
256+
throw MacroValidationError.invalidEnumValue(name, stringValue, allowed)
257257
}
258258
case .constant(let expected):
259259
if let stringValue = value as? String, stringValue != expected {
260-
throw ValidationError.notEqualToConstant(name, expected, stringValue)
260+
throw MacroValidationError.notEqualToConstant(name, expected, stringValue)
261261
}
262262
}
263263
}
264264
}
265265
}
266266

267267
/// Validation errors
268-
internal enum ValidationError: LocalizedError {
268+
internal enum MacroValidationError: LocalizedError {
269269
case belowMinimum(String, Int)
270270
case aboveMaximum(String, Int)
271271
case outOfRange(String, ClosedRange<Int>)
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Commented out for 0.0.1 release - focusing on simple text generation only
2+
/*
3+
import Foundation
4+
import MLX
5+
import MLXLLM
6+
import MLXLMCommon
7+
import Tokenizers
8+
9+
/// Simple JSON validation for generated content
10+
public struct SimpleJSONValidator {
11+
12+
/// Validate generated JSON against a schema with retry capability
13+
public static func validateAndRetry<T: Codable>(
14+
model: LanguageModel,
15+
tokenizer: Tokenizer,
16+
schema: [String: Any],
17+
maxRetries: Int = 3,
18+
prompt: String
19+
) async throws -> T {
20+
21+
for attempt in 1...maxRetries {
22+
print("🔄 Validation attempt \(attempt)/\(maxRetries)")
23+
24+
// Generate JSON
25+
let generatedText = try await generateText(
26+
model: model,
27+
tokenizer: tokenizer,
28+
prompt: prompt
29+
)
30+
31+
// Try to parse and validate
32+
if let validObject: T = try? parseAndValidate(
33+
jsonText: generatedText,
34+
schema: schema
35+
) {
36+
print("✅ Valid JSON generated on attempt \(attempt)")
37+
return validObject
38+
}
39+
40+
print("❌ Invalid JSON on attempt \(attempt), retrying...")
41+
}
42+
43+
throw ValidationError.maxRetriesExceeded
44+
}
45+
46+
/// Generate text using MLX model
47+
private static func generateText(
48+
model: LanguageModel,
49+
tokenizer: Tokenizer,
50+
prompt: String
51+
) async throws -> String {
52+
53+
let messages = [
54+
["role": "user", "content": prompt]
55+
]
56+
57+
let input = try await tokenizer.apply(chat: messages)
58+
let parameters = GenerateParameters(maxTokens: 1000, temperature: 0.1)
59+
60+
var generatedText = ""
61+
let generate = try await model.generate(
62+
input: input,
63+
parameters: parameters
64+
) { token in
65+
generatedText += tokenizer.decode(tokens: [token])
66+
return .more
67+
}
68+
69+
return generatedText
70+
}
71+
72+
/// Parse JSON and validate against schema
73+
private static func parseAndValidate<T: Codable>(
74+
jsonText: String,
75+
schema: [String: Any]
76+
) throws -> T {
77+
78+
// Extract JSON from response (handle cases with extra text)
79+
guard let cleanJSON = extractJSON(from: jsonText) else {
80+
throw ValidationError.noJSONFound
81+
}
82+
83+
// Basic JSON syntax validation
84+
guard let data = cleanJSON.data(using: .utf8),
85+
JSONSerialization.isValidJSONObject(try JSONSerialization.jsonObject(with: data))
86+
else {
87+
throw ValidationError.invalidJSONSyntax
88+
}
89+
90+
// Decode to target type
91+
let decoder = JSONDecoder()
92+
do {
93+
return try decoder.decode(T.self, from: data)
94+
} catch {
95+
throw ValidationError.schemaValidationFailed(error)
96+
}
97+
}
98+
99+
/// Extract JSON from potentially messy generated text
100+
private static func extractJSON(from text: String) -> String? {
101+
// Look for JSON blocks (between { and })
102+
let patterns = [
103+
"\\{[^{}]*(?:\\{[^{}]*\\}[^{}]*)*\\}", // Simple nested objects
104+
"```json\\s*([\\s\\S]*?)```", // Markdown JSON blocks
105+
"```\\s*([\\s\\S]*?)```", // Generic code blocks
106+
]
107+
108+
for pattern in patterns {
109+
if let range = text.range(of: pattern, options: .regularExpression) {
110+
var jsonCandidate = String(text[range])
111+
112+
// Clean up markdown markers
113+
jsonCandidate =
114+
jsonCandidate
115+
.replacingOccurrences(of: "```json", with: "")
116+
.replacingOccurrences(of: "```", with: "")
117+
.trimmingCharacters(in: .whitespacesAndNewlines)
118+
119+
return jsonCandidate
120+
}
121+
}
122+
123+
// Fallback: return the text as-is if it looks like JSON
124+
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
125+
if trimmed.hasPrefix("{") && trimmed.hasSuffix("}") {
126+
return trimmed
127+
}
128+
129+
return nil
130+
}
131+
}
132+
133+
/// Validation errors
134+
public enum ValidationError: Error, LocalizedError {
135+
case maxRetriesExceeded
136+
case noJSONFound
137+
case invalidJSONSyntax
138+
case schemaValidationFailed(Error)
139+
140+
public var errorDescription: String? {
141+
switch self {
142+
case .maxRetriesExceeded:
143+
return "Failed to generate valid JSON after maximum retries"
144+
case .noJSONFound:
145+
return "No JSON found in generated text"
146+
case .invalidJSONSyntax:
147+
return "Generated text is not valid JSON"
148+
case .schemaValidationFailed(let error):
149+
return "Schema validation failed: \(error.localizedDescription)"
150+
}
151+
}
152+
}
153+
*/

0 commit comments

Comments
 (0)