Skip to content

Commit 6dcc845

Browse files
authored
Add grok (#9)
* feat: add Grok (xAI) client implementation - Implement GrokClient with OpenAI-compatible API interface - Add Grok model enum with latest model variants - Enable configurable timeouts matching other clients - Update feature flags to include grok by default - Add documentation and examples for Grok client usage * docs: add model list maintenance guidelines - Add comprehensive documentation for model list updates - Include official documentation links for OpenAI, Anthropic, and xAI - Update Grok models with latest model variants (Grok-3, Grok-4) - Add validation test for Grok client implementation - Update model enum documentation with reference links * docs: update model lists with latest versions - Update OpenAI models with GPT-5 and O1 series - Update Anthropic models with Claude 4 and 4.5 series - Add comprehensive model maintenance guidelines - Update examples and tests to use latest models * feat: add markdown code block JSON extraction - Add helper function to extract JSON from markdown code blocks - Apply extraction before JSON parsing in Anthropic client - Improve error messages with extracted content - Maintain fallback to raw content when no code blocks found
1 parent 74766f5 commit 6dcc845

17 files changed

Lines changed: 1113 additions & 47 deletions

CLAUDE.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,55 @@
3737
- Never use JSON serialized strings for attribute values (e.g., do not use `examples = r#"["value1"]"#`)
3838
- Parse container attributes individually without relying on JSON parsing
3939
- Support multiple attribute specification styles that feel natural in Rust
40-
- For multi-value attributes, support both parentheses and array syntax
40+
- For multi-value attributes, support both parentheses and array syntax
41+
42+
## Model List Maintenance Guidelines
43+
44+
**CRITICAL**: Model lists must be kept up-to-date with provider releases. When adding new providers or updating existing ones, always refer to the official documentation sources below:
45+
46+
### Official Model Documentation Sources
47+
48+
**OpenAI:**
49+
- Model Documentation: https://platform.openai.com/docs/models
50+
- Models API Endpoint: https://platform.openai.com/docs/api-reference/models/list
51+
52+
**Anthropic:**
53+
- All Models Overview: https://docs.anthropic.com/en/docs/about-claude/models/all-models
54+
- Models List API: https://docs.anthropic.com/en/api/models-list
55+
56+
**xAI (Grok):**
57+
- Models Documentation: https://docs.x.ai/docs/models
58+
59+
### Update Process
60+
61+
**CRITICAL RULES:**
62+
1. **ALWAYS use API endpoints to get current models** - Never guess or rely on web search. Use the official API endpoints:
63+
- **OpenAI**: `GET https://api.openai.com/v1/models` (requires `Authorization: Bearer $OPENAI_API_KEY`)
64+
- **Anthropic**: `GET https://api.anthropic.com/v1/models` (requires `x-api-key: $ANTHROPIC_API_KEY` and `anthropic-version: 2023-06-01`)
65+
- **xAI (Grok)**: Check `https://docs.x.ai/docs/models` or use their API if available
66+
2. **NEVER guess model identifiers** - Always get exact model names from API responses or official documentation
67+
3. **NEVER rely on web search results** - Web search often returns outdated, incorrect, or speculative information
68+
4. **ALWAYS use exact API identifiers** - Model identifiers must match exactly what the API expects (e.g., `gpt-5-chat-latest`, `claude-sonnet-4-5-20250929`, `grok-4-0709`)
69+
5. **If API key is not available**: Ask the user to provide the exact model identifiers from the API endpoint, rather than guessing
70+
6. **Verify date stamps make sense** - If version X.Y is newer than X.Z, its date stamp should be later (e.g., Claude 3.7 date should be after Claude 3.5 date)
71+
7. **Check for new major versions** - Don't assume only minor version updates; check for major version releases (e.g., Claude 4, GPT-5)
72+
8. **Verify model name format** - Different providers may use different naming conventions (e.g., `claude-sonnet-4-20250514` vs `claude-4-sonnet-20250514`)
73+
9. **Filter for chat completion models** - Only include models suitable for chat completions (exclude specialized models like search-api, codex, audio, etc. unless specifically needed)
74+
75+
**Steps:**
76+
1. **Get current models from API**: Use `curl` or API calls to fetch the latest model list from the provider's `/v1/models` endpoint
77+
2. **Parse API response**: Extract model `id` fields from the JSON response
78+
3. **Filter appropriate models**: For chat completions, include main chat models and exclude specialized variants unless needed
79+
4. **Verify model identifiers**: Ensure date stamps and version numbers are correct (e.g., Claude 3.7 should have a date later than Claude 3.5)
80+
5. **When updating**: Add new models to the appropriate enum (`Model`, `AnthropicModel`, `GrokModel`) in the respective backend files, ordered newest to oldest
81+
6. **Remove deprecated models**: Check API response for models that are no longer available and remove them
82+
7. **Default models**: Update default model selection to use the latest recommended model when appropriate
83+
8. **Documentation**: Update rustdoc comments to reference the official documentation links
84+
9. **Testing**: Ensure new models work correctly with integration tests
85+
86+
### Periodic Review Schedule
87+
88+
- Review model lists quarterly or when new models are announced
89+
- Check for deprecated models and remove or mark them as deprecated
90+
- Update default model selections to use the latest recommended models
91+
- Verify all model identifiers match current API documentation

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ chrono = "0.4" # For date/time validation in examples
3737

3838
# Feature flags
3939
[features]
40-
default = ["openai", "anthropic", "derive", "logging"]
40+
default = ["openai", "anthropic", "grok", "derive", "logging"]
4141
openai = ["reqwest", "tokio"]
4242
anthropic = ["reqwest", "tokio"]
43+
grok = ["reqwest", "tokio"]
4344
derive = ["rstructor_derive"]
4445
logging = ["tracing-subscriber", "tracing-futures"]
4546

README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Think of it as the Rust equivalent of [Instructor + Pydantic](https://github.com
1616
- **📝 Type-Safe Definitions**: Define data models as standard Rust structs/enums with attributes
1717
- **🔄 JSON Schema Generation**: Auto-generates JSON Schema from your Rust types
1818
- **✅ Built-in Validation**: Type checking plus custom business rule validation
19-
- **🔌 Multiple LLM Providers**: Support for OpenAI and Anthropic, with an extensible backend system
19+
- **🔌 Multiple LLM Providers**: Support for OpenAI, Anthropic, and Grok (xAI), with an extensible backend system
2020
- **🧩 Complex Data Structures**: Support for nested objects, arrays, optional fields, and deeply nested enums
2121
- **🧠 Schema Fidelity**: Heuristic-free JSON Schema generation that preserves nested struct and enum detail
2222
- **🔍 Custom Validation Rules**: Add domain-specific validation with automatically detected `validate` methods
@@ -471,16 +471,24 @@ let openai_client = OpenAIClient::new(openai_api_key)?
471471

472472
// Using Anthropic
473473
let anthropic_client = AnthropicClient::new(anthropic_api_key)?
474-
.model(AnthropicModel::Claude3Sonnet)
474+
.model(AnthropicModel::ClaudeSonnet4)
475475
.temperature(0.0)
476476
.max_tokens(2000)
477477
.with_timeout(Duration::from_secs(60)) // Optional: set 60 second timeout
478478
.build();
479+
480+
// Using Grok (xAI) - automatically uses XAI_API_KEY env var if empty string provided
481+
let grok_client = GrokClient::new("")? // Uses XAI_API_KEY env var
482+
.model(GrokModel::Grok4)
483+
.temperature(0.0)
484+
.max_tokens(1500)
485+
.with_timeout(Duration::from_secs(60)) // Optional: set 60 second timeout
486+
.build();
479487
```
480488

481489
### Configuring Request Timeouts
482490

483-
Both `OpenAIClient` and `AnthropicClient` support configurable timeouts for HTTP requests using the builder pattern:
491+
All clients (`OpenAIClient`, `AnthropicClient`, and `GrokClient`) support configurable timeouts for HTTP requests using the builder pattern:
484492

485493
```rust
486494
use std::time::Duration;
@@ -668,12 +676,13 @@ Configure RStructor with feature flags:
668676

669677
```toml
670678
[dependencies]
671-
rstructor = { version = "0.1.0", features = ["openai", "anthropic"] }
679+
rstructor = { version = "0.1.0", features = ["openai", "anthropic", "grok"] }
672680
```
673681

674682
Available features:
675683
- `openai`: Include the OpenAI client
676684
- `anthropic`: Include the Anthropic client
685+
- `grok`: Include the Grok (xAI) client
677686
- `derive`: Include the derive macro (enabled by default)
678687
- `logging`: Enable tracing integration with default subscriber
679688

examples/event_planner.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
317317
println!("\nProcessing your request with Anthropic...\n");
318318

319319
let client = AnthropicClient::new(api_key)?
320-
.model(AnthropicModel::Claude3Sonnet)
320+
.model(AnthropicModel::ClaudeSonnet4)
321321
.temperature(0.3)
322322
.build();
323323

examples/nested_objects_example.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ CRITICAL REQUIREMENTS - ALL FIELDS ARE REQUIRED:
204204
println!("Using Anthropic to generate recipe...");
205205

206206
let client = AnthropicClient::new(api_key)?
207-
.model(AnthropicModel::Claude3Sonnet) // Using more capable model for complex structure
207+
.model(AnthropicModel::ClaudeSonnet4) // Using more capable model for complex structure
208208
.temperature(0.2)
209209
.build();
210210

examples/news_article_categorizer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ async fn analyze_article(
146146
println!("Using Anthropic for article analysis...");
147147

148148
let client = AnthropicClient::new(api_key)?
149-
.model(AnthropicModel::Claude3Sonnet)
149+
.model(AnthropicModel::ClaudeSonnet4)
150150
.temperature(0.0)
151151
.build();
152152

examples/recipe_extractor.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ async fn get_recipe_from_anthropic(recipe_name: &str) -> rstructor::Result<Recip
190190

191191
// Create Anthropic client
192192
let client = AnthropicClient::new(api_key)?
193-
.model(AnthropicModel::Claude35Sonnet) // Use Claude 3.5 Sonnet for better recipes
193+
.model(AnthropicModel::ClaudeSonnet45) // Use Claude Sonnet 4.5 for better recipes
194194
.temperature(0.1) // Lower temperature for more consistent results
195195
.build();
196196

src/backend/anthropic.rs

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,65 @@ use crate::backend::LLMClient;
88
use crate::error::{RStructorError, Result};
99
use crate::model::Instructor;
1010

11+
/// Extract JSON from markdown code blocks if present, otherwise return the content as-is
12+
fn extract_json_from_markdown(content: &str) -> String {
13+
// Check if content is wrapped in markdown code blocks
14+
let trimmed = content.trim();
15+
16+
// Match ```json ... ``` or ``` ... ```
17+
if trimmed.starts_with("```") {
18+
// Find the first newline after ```
19+
if let Some(start_idx) = trimmed.find('\n') {
20+
let after_start = &trimmed[start_idx + 1..];
21+
// Find the closing ```
22+
if let Some(end_idx) = after_start.rfind("```") {
23+
return after_start[..end_idx].trim().to_string();
24+
}
25+
}
26+
}
27+
28+
// If no markdown code blocks found, return as-is
29+
trimmed.to_string()
30+
}
31+
1132
/// Anthropic models available for completion
33+
///
34+
/// For the latest available models and their identifiers, check the
35+
/// [Anthropic Models Documentation](https://docs.anthropic.com/en/docs/about-claude/models/all-models).
1236
#[derive(Debug, Clone)]
1337
pub enum AnthropicModel {
38+
/// Claude Haiku 4.5 (latest fastest model)
39+
ClaudeHaiku45,
40+
/// Claude Sonnet 4.5 (latest balanced model)
41+
ClaudeSonnet45,
42+
/// Claude Opus 4.1 (enhanced reasoning capabilities)
43+
ClaudeOpus41,
44+
/// Claude Opus 4 (high-intelligence model)
45+
ClaudeOpus4,
46+
/// Claude Sonnet 4 (balanced performance model)
47+
ClaudeSonnet4,
48+
/// Claude Sonnet 3.7 (enhanced reasoning)
49+
Claude37Sonnet,
50+
/// Claude Haiku 3.5 (fast, cost-effective model)
51+
Claude35Haiku,
52+
/// Claude Haiku 3 (fast, cost-effective model)
1453
Claude3Haiku,
15-
Claude3Sonnet,
54+
/// Claude Opus 3 (most capable model for complex tasks)
1655
Claude3Opus,
17-
Claude35Sonnet, // Added Claude 3.5 Sonnet
1856
}
1957

2058
impl AnthropicModel {
2159
pub fn as_str(&self) -> &'static str {
2260
match self {
61+
AnthropicModel::ClaudeHaiku45 => "claude-haiku-4-5-20251001",
62+
AnthropicModel::ClaudeSonnet45 => "claude-sonnet-4-5-20250929",
63+
AnthropicModel::ClaudeOpus41 => "claude-opus-4-1-20250805",
64+
AnthropicModel::ClaudeOpus4 => "claude-opus-4-20250514",
65+
AnthropicModel::ClaudeSonnet4 => "claude-sonnet-4-20250514",
66+
AnthropicModel::Claude37Sonnet => "claude-3-7-sonnet-20250219",
67+
AnthropicModel::Claude35Haiku => "claude-3-5-haiku-20241022",
2368
AnthropicModel::Claude3Haiku => "claude-3-haiku-20240307",
24-
AnthropicModel::Claude3Sonnet => "claude-3-sonnet-20240229",
2569
AnthropicModel::Claude3Opus => "claude-3-opus-20240229",
26-
AnthropicModel::Claude35Sonnet => "claude-3-5-sonnet-20240620", // Claude 3.5 Sonnet model ID
2770
}
2871
}
2972
}
@@ -80,15 +123,15 @@ struct CompletionResponse {
80123

81124
impl AnthropicClient {
82125
/// Create a new Anthropic client with default configuration
83-
#[instrument(name = "anthropic_client_new", skip(api_key), fields(model = ?AnthropicModel::Claude35Sonnet))]
126+
#[instrument(name = "anthropic_client_new", skip(api_key), fields(model = ?AnthropicModel::ClaudeSonnet45))]
84127
pub fn new(api_key: impl Into<String>) -> Result<Self> {
85128
let api_key = api_key.into();
86129
info!("Creating new Anthropic client");
87130
trace!("API key length: {}", api_key.len());
88131

89132
let config = AnthropicConfig {
90133
api_key,
91-
model: AnthropicModel::Claude35Sonnet, // Default to Claude 3.5 Sonnet
134+
model: AnthropicModel::ClaudeSonnet45, // Default to Claude Sonnet 4.5 (latest flagship)
92135
temperature: 0.0,
93136
max_tokens: None,
94137
timeout: None, // Default: no timeout (uses reqwest's default)
@@ -306,17 +349,19 @@ impl LLMClient for AnthropicClient {
306349
};
307350

308351
// Try to parse the content as JSON
309-
trace!(json = %content, "Attempting to parse response as JSON");
310-
let result: T = match serde_json::from_str(content) {
352+
// First, try to extract JSON from markdown code blocks if present
353+
let json_content = extract_json_from_markdown(content);
354+
trace!(json = %json_content, "Attempting to parse response as JSON");
355+
let result: T = match serde_json::from_str(&json_content) {
311356
Ok(parsed) => parsed,
312357
Err(e) => {
313358
let error_msg = format!(
314359
"Failed to parse response as JSON: {}\nPartial JSON: {}",
315-
e, content
360+
e, json_content
316361
);
317362
error!(
318363
error = %e,
319-
content = %content,
364+
content = %json_content,
320365
"JSON parsing error"
321366
);
322367
return Err(RStructorError::ValidationError(error_msg));

src/backend/client.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use crate::model::Instructor;
1616
/// The library includes implementations for popular LLM providers:
1717
/// - `OpenAIClient` for OpenAI's GPT models (gpt-3.5-turbo, gpt-4, etc.)
1818
/// - `AnthropicClient` for Anthropic's Claude models
19+
/// - `GrokClient` for xAI's Grok models (uses `XAI_API_KEY` env var by default)
1920
///
2021
/// # Examples
2122
///

0 commit comments

Comments
 (0)