Skip to content

Commit af1ccc4

Browse files
committed
feat: add support for openrouter
1 parent 36cd17f commit af1ccc4

File tree

8 files changed

+7359
-9
lines changed

8 files changed

+7359
-9
lines changed

CLAUDE.md

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,110 @@ Single provider JSON:
8282

8383
## Adding New Providers
8484

85-
1. Create new provider file in `src/providers/`
86-
2. Implement the `Provider` trait
87-
3. Add to `src/providers/mod.rs`
88-
4. Register in `src/main.rs`
85+
### Step-by-Step Guide
86+
87+
1. **Create Provider Implementation**
88+
- Create new file in `src/providers/` (e.g., `src/providers/newprovider.rs`)
89+
- Implement the `Provider` trait with required methods:
90+
- `async fn fetch_models(&self) -> Result<Vec<ModelInfo>>`
91+
- `fn provider_id(&self) -> &str`
92+
- `fn provider_name(&self) -> &str`
93+
94+
2. **Add Module Reference**
95+
- Add `pub mod newprovider;` to `src/providers/mod.rs`
96+
97+
3. **Register in Main**
98+
- Import the provider in `src/main.rs`: `providers::newprovider::NewProviderProvider`
99+
- Add provider initialization in `fetch_all_providers()` function
100+
101+
4. **Update Documentation**
102+
- Add JSON link to README.md "Available Model Data" section
103+
- Update "Currently Supported Providers" section with provider status
104+
105+
### Template for New Provider
106+
107+
```rust
108+
use async_trait::async_trait;
109+
use anyhow::Result;
110+
use serde::Deserialize;
111+
use crate::models::{ModelInfo, ModelType};
112+
use crate::providers::Provider;
113+
114+
#[derive(Debug, Deserialize)]
115+
struct NewProviderModel {
116+
// Define API response structure
117+
}
118+
119+
#[derive(Debug, Deserialize)]
120+
struct NewProviderResponse {
121+
// Define API response wrapper
122+
}
123+
124+
pub struct NewProviderProvider {
125+
api_url: String,
126+
client: reqwest::Client,
127+
}
128+
129+
impl NewProviderProvider {
130+
pub fn new(api_url: String) -> Self {
131+
Self {
132+
api_url,
133+
client: reqwest::Client::new(),
134+
}
135+
}
136+
137+
fn convert_model(&self, model: NewProviderModel) -> ModelInfo {
138+
// Convert API model to standardized ModelInfo
139+
// Detect capabilities: vision, function_call, reasoning
140+
ModelInfo::new(
141+
model.id,
142+
model.name,
143+
context_length,
144+
max_tokens,
145+
vision,
146+
function_call,
147+
reasoning,
148+
ModelType::Chat,
149+
description,
150+
)
151+
}
152+
}
153+
154+
#[async_trait]
155+
impl Provider for NewProviderProvider {
156+
async fn fetch_models(&self) -> Result<Vec<ModelInfo>> {
157+
let response = self.client
158+
.get(&self.api_url)
159+
.send()
160+
.await?
161+
.json::<NewProviderResponse>()
162+
.await?;
163+
164+
let models = response.data
165+
.into_iter()
166+
.map(|model| self.convert_model(model))
167+
.collect();
168+
169+
Ok(models)
170+
}
171+
172+
fn provider_id(&self) -> &str {
173+
"newprovider"
174+
}
175+
176+
fn provider_name(&self) -> &str {
177+
"New Provider"
178+
}
179+
}
180+
```
181+
182+
### After Adding Provider
183+
184+
The system will automatically:
185+
- Generate `{provider_id}.json` file in `dist/` directory
186+
- Include provider data in `all.json` aggregated file
187+
- Update GitHub Actions to fetch from new provider
188+
- Create downloadable JSON links in releases
89189

90190
## GitHub Actions
91191

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ Automated tool to fetch AI model information from various providers (PPInfra, Op
1111
- 🎯 **Aggregated Output**: Generate both individual provider files and complete aggregated files
1212
- 🚀 **GitHub Actions**: Automated scheduled updates for model information
1313

14+
### 📄 Available Model Data
15+
16+
Access the latest AI model information in JSON format:
17+
18+
- **All Providers Combined**: [all.json](https://github.com/zerob13/PublicProviderConf/raw/main/provider_configs/all.json) - Complete aggregated data from all providers
19+
- **PPInfra**: [ppinfra.json](https://github.com/zerob13/PublicProviderConf/raw/main/provider_configs/ppinfra.json) - PPInfra provider models
20+
- **OpenRouter**: [openrouter.json](https://github.com/zerob13/PublicProviderConf/raw/main/provider_configs/openrouter.json) - OpenRouter provider models
21+
1422
## 📦 Installation
1523

1624
### Prerequisites
@@ -217,7 +225,7 @@ For detailed development guide, see [Architecture Documentation](docs/architectu
217225
## 📊 Currently Supported Providers
218226

219227
-**PPInfra** - 38 models with reasoning, function calling, and vision capability detection
220-
- 🚧 **OpenRouter** - Planned
228+
- **OpenRouter** - 600+ models with comprehensive capability detection and metadata
221229
- 🚧 **OpenAI** - Planned
222230
- 🚧 **Google Gemini** - Planned
223231

dist/all.json

Lines changed: 3550 additions & 2 deletions
Large diffs are not rendered by default.

dist/openrouter.json

Lines changed: 3549 additions & 0 deletions
Large diffs are not rendered by default.

dist/ppinfra.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"provider": "ppinfra",
33
"providerName": "PPInfra",
4-
"lastUpdated": "2025-08-30T09:13:28.078738Z",
4+
"lastUpdated": "2025-08-30T10:30:25.289860Z",
55
"models": [
66
{
77
"id": "deepseek/deepseek-v3.1",

src/main.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use anyhow::Result;
44

55
use public_provider_conf::{
66
providers::ppinfra::PPInfraProvider,
7+
providers::openrouter::OpenRouterProvider,
78
fetcher::DataFetcher,
89
output::OutputManager,
910
};
@@ -63,12 +64,18 @@ async fn fetch_all_providers(output_dir: String, _config_path: String) -> Result
6364

6465
let mut fetcher = DataFetcher::new();
6566

66-
// Add PPInfra provider as example
67+
// Add PPInfra provider
6768
let ppinfra = Arc::new(PPInfraProvider::new(
6869
"https://api.ppinfra.com/openai/v1/models".to_string()
6970
));
7071
fetcher.add_provider(ppinfra);
7172

73+
// Add OpenRouter provider
74+
let openrouter = Arc::new(OpenRouterProvider::new(
75+
"https://openrouter.ai/api/v1/models".to_string()
76+
));
77+
fetcher.add_provider(openrouter);
78+
7279
let provider_data = fetcher.fetch_all().await?;
7380

7481
let output_manager = OutputManager::new(output_dir);

src/providers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod ppinfra;
2+
pub mod openrouter;
23

34
use async_trait::async_trait;
45
use anyhow::Result;

src/providers/openrouter.rs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use async_trait::async_trait;
2+
use anyhow::Result;
3+
use serde::Deserialize;
4+
use crate::models::{ModelInfo, ModelType};
5+
use crate::providers::Provider;
6+
7+
#[derive(Debug, Deserialize)]
8+
struct OpenRouterArchitecture {
9+
modality: Option<String>,
10+
#[serde(rename = "input_modalities")]
11+
input_modalities: Option<Vec<String>>,
12+
#[serde(rename = "output_modalities")]
13+
output_modalities: Option<Vec<String>>,
14+
tokenizer: Option<String>,
15+
#[serde(rename = "instruct_type")]
16+
instruct_type: Option<String>,
17+
}
18+
19+
#[derive(Debug, Deserialize)]
20+
struct OpenRouterTopProvider {
21+
#[serde(rename = "is_moderated")]
22+
is_moderated: Option<bool>,
23+
#[serde(rename = "context_length")]
24+
context_length: Option<u32>,
25+
#[serde(rename = "max_completion_tokens")]
26+
max_completion_tokens: Option<u32>,
27+
}
28+
29+
#[derive(Debug, Deserialize)]
30+
struct OpenRouterModel {
31+
id: String,
32+
#[serde(rename = "canonical_slug")]
33+
canonical_slug: Option<String>,
34+
#[serde(rename = "hugging_face_id")]
35+
hugging_face_id: Option<String>,
36+
name: String,
37+
created: Option<i64>,
38+
description: Option<String>,
39+
architecture: Option<OpenRouterArchitecture>,
40+
#[serde(rename = "top_provider")]
41+
top_provider: Option<OpenRouterTopProvider>,
42+
#[serde(rename = "context_length")]
43+
context_length: u32,
44+
#[serde(rename = "per_request_limits")]
45+
per_request_limits: Option<serde_json::Value>,
46+
#[serde(rename = "supported_parameters")]
47+
supported_parameters: Option<Vec<String>>,
48+
}
49+
50+
#[derive(Debug, Deserialize)]
51+
struct OpenRouterResponse {
52+
data: Vec<OpenRouterModel>,
53+
}
54+
55+
pub struct OpenRouterProvider {
56+
api_url: String,
57+
client: reqwest::Client,
58+
}
59+
60+
impl OpenRouterProvider {
61+
pub fn new(api_url: String) -> Self {
62+
Self {
63+
api_url,
64+
client: reqwest::Client::new(),
65+
}
66+
}
67+
68+
fn convert_model(&self, model: OpenRouterModel) -> ModelInfo {
69+
let vision = model.architecture
70+
.as_ref()
71+
.and_then(|arch| arch.input_modalities.as_ref())
72+
.map(|modalities| modalities.iter().any(|m| m.contains("image")))
73+
.unwrap_or(false);
74+
75+
let function_call = model.supported_parameters
76+
.as_ref()
77+
.map(|params| params.iter().any(|p| p.contains("tool") || p.contains("function")))
78+
.unwrap_or(false);
79+
80+
let reasoning = model.supported_parameters
81+
.as_ref()
82+
.map(|params| params.iter().any(|p| p.contains("reasoning") || p.contains("include_reasoning")))
83+
.unwrap_or(false);
84+
85+
let max_tokens = model.top_provider
86+
.as_ref()
87+
.and_then(|tp| tp.max_completion_tokens)
88+
.unwrap_or(4096);
89+
90+
let context_length = model.top_provider
91+
.as_ref()
92+
.and_then(|tp| tp.context_length)
93+
.unwrap_or(model.context_length);
94+
95+
ModelInfo::new(
96+
model.id,
97+
model.name,
98+
context_length,
99+
max_tokens,
100+
vision,
101+
function_call,
102+
reasoning,
103+
ModelType::Chat,
104+
model.description,
105+
)
106+
}
107+
}
108+
109+
#[async_trait]
110+
impl Provider for OpenRouterProvider {
111+
async fn fetch_models(&self) -> Result<Vec<ModelInfo>> {
112+
let response_text = self.client
113+
.get(&self.api_url)
114+
.send()
115+
.await?
116+
.text()
117+
.await?;
118+
119+
let response: OpenRouterResponse = serde_json::from_str(&response_text)
120+
.map_err(|e| anyhow::anyhow!("Failed to parse OpenRouter response: {}\nFirst 1000 chars: {}", e, &response_text[..std::cmp::min(1000, response_text.len())]))?;
121+
122+
let models = response.data
123+
.into_iter()
124+
.map(|model| self.convert_model(model))
125+
.collect();
126+
127+
Ok(models)
128+
}
129+
130+
fn provider_id(&self) -> &str {
131+
"openrouter"
132+
}
133+
134+
fn provider_name(&self) -> &str {
135+
"OpenRouter"
136+
}
137+
}

0 commit comments

Comments
 (0)