From 54a7639dc4f34769f68b58b0aeead5013beaa43a Mon Sep 17 00:00:00 2001 From: Joshua Scott Date: Sun, 19 Apr 2026 18:55:30 -0600 Subject: [PATCH 1/2] Added static model support for the .forge.toml --- .gitignore | 1 + crates/forge_config/src/config.rs | 230 +++++++++++++++++- .../forge_repo/src/provider/provider_repo.rs | 30 ++- forge.schema.json | 103 +++++++- ...2026-04-17-static-model-list-support-v1.md | 215 ++++++++++++++++ 5 files changed, 571 insertions(+), 8 deletions(-) create mode 100644 plans/2026-04-17-static-model-list-support-v1.md diff --git a/.gitignore b/.gitignore index ae359692b2..c7192784e1 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ jobs/** node_modules/ bench/__pycache__ .ai/ +.forge/FORGE_EDITMSG.md diff --git a/crates/forge_config/src/config.rs b/crates/forge_config/src/config.rs index 6b9baaa213..eeedd181ad 100644 --- a/crates/forge_config/src/config.rs +++ b/crates/forge_config/src/config.rs @@ -59,6 +59,75 @@ pub struct ProviderUrlParam { pub options: Vec, } +/// Input modality supported by a model. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Dummy)] +#[serde(rename_all = "lowercase")] +pub enum InputModality { + Text, + Image, +} + +/// Default input modalities when not specified (text-only). +fn default_input_modalities() -> Vec { + vec![InputModality::Text] +} + +/// A static model entry for inline provider configuration. +/// +/// This allows defining model capabilities directly in `forge.toml` without +/// requiring a URL-based model discovery. +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Dummy, Setters)] +#[serde(rename_all = "snake_case")] +#[setters(strip_option)] +pub struct StaticModelEntry { + /// Unique model identifier (e.g. `"gpt-4"`). + pub id: String, + /// Human-readable model name. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, + /// Description of the model. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Maximum context window size in tokens. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub context_length: Option, + /// Whether the model supports tool calls. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tools_supported: Option, + /// Whether the model supports parallel tool calls. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub supports_parallel_tool_calls: Option, + /// Whether the model supports reasoning. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub supports_reasoning: Option, + /// Input modalities supported by the model (defaults to text-only). + #[serde(default = "default_input_modalities")] + pub input_modalities: Vec, +} + +/// Model source for a provider. +/// +/// This can be either a URL template for fetching the model list, or a +/// static list of model entries defined inline. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(untagged)] +pub enum ProviderModels { + /// URL template for fetching the model list. + Url(String), + /// Static list of model entries. + Static(Vec), +} + +impl fake::Dummy for ProviderModels { + fn dummy_with_rng(_: &fake::Faker, rng: &mut R) -> Self { + // Generate a static list of models for testing + Self::Static(vec![StaticModelEntry::dummy_with_rng( + &fake::Faker, + rng, + )]) + } +} + /// A single provider entry defined inline in `forge.toml`. /// /// Inline providers are merged with the built-in provider list; entries with @@ -75,10 +144,10 @@ pub struct ProviderEntry { /// URL template for chat completions; may contain `{{VAR}}` placeholders /// that are substituted from the credential's url params. pub url: String, - /// URL template for fetching the model list; may contain `{{VAR}}` - /// placeholders. + /// Model source for this provider. Can be either a URL template for + /// fetching the model list, or a static list of model entries. #[serde(default, skip_serializing_if = "Option::is_none")] - pub models: Option, + pub models: Option, /// Wire protocol used by this provider. #[serde(default, skip_serializing_if = "Option::is_none")] pub response_type: Option, @@ -353,4 +422,159 @@ mod tests { assert_eq!(actual.temperature, fixture.temperature); } + + #[test] + fn test_provider_models_url() { + let json = r#""https://api.example.com/models""#; + let models: ProviderModels = serde_json::from_str(json).unwrap(); + assert!(matches!(models, ProviderModels::Url(url) if url == "https://api.example.com/models")); + } + + #[test] + fn test_provider_models_static() { + let json = r#"[{"id": "gpt-4", "name": "GPT-4", "context_length": 128000}]"#; + let models: ProviderModels = serde_json::from_str(json).unwrap(); + + match models { + ProviderModels::Static(entries) => { + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].id, "gpt-4"); + assert_eq!(entries[0].name, Some("GPT-4".to_string())); + assert_eq!(entries[0].context_length, Some(128000)); + } + other => panic!("Expected Static variant, got {:?}", other), + } + } + + #[test] + fn test_provider_models_multiple_entries() { + let json = r#"[ + {"id": "gpt-4", "tools_supported": true}, + {"id": "gpt-3.5-turbo", "tools_supported": false} + ]"#; + let models: ProviderModels = serde_json::from_str(json).unwrap(); + + match models { + ProviderModels::Static(entries) => { + assert_eq!(entries.len(), 2); + assert_eq!(entries[0].id, "gpt-4"); + assert_eq!(entries[0].tools_supported, Some(true)); + assert_eq!(entries[1].id, "gpt-3.5-turbo"); + assert_eq!(entries[1].tools_supported, Some(false)); + } + other => panic!("Expected Static variant, got {:?}", other), + } + } + + #[test] + fn test_provider_models_round_trip() { + let original = ProviderModels::Static(vec![ + StaticModelEntry { + id: "qwen3-35b".to_string(), + name: Some("Qwen 3.5 35B".to_string()), + description: Some("Local reasoning model".to_string()), + context_length: Some(262144), + tools_supported: Some(true), + supports_parallel_tool_calls: Some(true), + supports_reasoning: Some(true), + input_modalities: vec![InputModality::Text], + }, + ]); + + let json = serde_json::to_string(&original).unwrap(); + let parsed: ProviderModels = serde_json::from_str(&json).unwrap(); + + match (&original, &parsed) { + (ProviderModels::Static(orig_entries), ProviderModels::Static(parsed_entries)) => { + assert_eq!(parsed_entries.len(), 1); + assert_eq!(parsed_entries[0].id, "qwen3-35b"); + assert_eq!(parsed_entries[0].name, orig_entries[0].name); + assert_eq!(parsed_entries[0].context_length, orig_entries[0].context_length); + assert_eq!(parsed_entries[0].tools_supported, orig_entries[0].tools_supported); + } + other => panic!("Expected Static variants, got {:?}", other), + } + } + + #[test] + fn test_provider_models_input_modalities_default() { + let json = r#"[{"id": "test-model"}]"#; + let models: ProviderModels = serde_json::from_str(json).unwrap(); + + match models { + ProviderModels::Static(entries) => { + assert_eq!(entries[0].input_modalities, vec![InputModality::Text]); + } + other => panic!("Expected Static variant, got {:?}", other), + } + } + + #[test] + fn test_provider_models_with_image_modality() { + let json = r#"[{"id": "vision-model", "input_modalities": ["text", "image"]}]"#; + let models: ProviderModels = serde_json::from_str(json).unwrap(); + + match models { + ProviderModels::Static(entries) => { + assert_eq!( + entries[0].input_modalities, + vec![InputModality::Text, InputModality::Image] + ); + } + other => panic!("Expected Static variant, got {:?}", other), + } + } + + #[test] + fn test_provider_entry_with_static_models() { + let json = r#"{ + "id": "ollama", + "url": "http://localhost:8000/v1/chat/completions", + "models": [ + { + "id": "qwen3-35b", + "name": "Qwen 3.5 35B", + "context_length": 262144, + "tools_supported": true, + "supports_parallel_tool_calls": true, + "supports_reasoning": true, + "input_modalities": ["text"] + } + ] + }"#; + + let entry: ProviderEntry = serde_json::from_str(json).unwrap(); + assert_eq!(entry.id, "ollama"); + assert_eq!(entry.url, "http://localhost:8000/v1/chat/completions"); + + match entry.models { + Some(ProviderModels::Static(entries)) => { + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].id, "qwen3-35b"); + assert_eq!(entries[0].context_length, Some(262144)); + assert_eq!(entries[0].tools_supported, Some(true)); + assert_eq!(entries[0].supports_reasoning, Some(true)); + } + other => panic!("Expected Static variant, got {:?}", other), + } + } + + #[test] + fn test_provider_entry_with_url_models() { + let json = r#"{ + "id": "openai", + "url": "https://api.openai.com/v1/chat/completions", + "models": "https://api.openai.com/v1/models" + }"#; + + let entry: ProviderEntry = serde_json::from_str(json).unwrap(); + assert_eq!(entry.id, "openai"); + + match entry.models { + Some(ProviderModels::Url(url)) => { + assert_eq!(url, "https://api.openai.com/v1/models"); + } + other => panic!("Expected Url variant, got {:?}", other), + } + } } diff --git a/crates/forge_repo/src/provider/provider_repo.rs b/crates/forge_repo/src/provider/provider_repo.rs index d17c1c25c5..52db52207b 100644 --- a/crates/forge_repo/src/provider/provider_repo.rs +++ b/crates/forge_repo/src/provider/provider_repo.rs @@ -155,7 +155,35 @@ impl From for ProviderConfig { url_param_vars: entry.url_param_vars.into_iter().map(Into::into).collect(), response_type, url: entry.url, - models: entry.models.map(Models::Url), + models: entry.models.map(|m| match m { + forge_config::ProviderModels::Url(url) => Models::Url(url), + forge_config::ProviderModels::Static(entries) => Models::Hardcoded( + entries + .into_iter() + .map(|e| forge_app::domain::Model { + id: e.id.into(), + name: e.name, + description: e.description, + context_length: e.context_length, + tools_supported: e.tools_supported, + supports_parallel_tool_calls: e.supports_parallel_tool_calls, + supports_reasoning: e.supports_reasoning, + input_modalities: e + .input_modalities + .into_iter() + .map(|m| match m { + forge_config::InputModality::Text => { + forge_app::domain::InputModality::Text + } + forge_config::InputModality::Image => { + forge_app::domain::InputModality::Image + } + }) + .collect(), + }) + .collect(), + ), + }), auth_methods, custom_headers: entry.custom_headers, } diff --git a/forge.schema.json b/forge.schema.json index 43cc190609..c3e07e6902 100644 --- a/forge.schema.json +++ b/forge.schema.json @@ -584,6 +584,14 @@ "accept_invalid_certs" ] }, + "InputModality": { + "description": "Input modality supported by a model.", + "type": "string", + "enum": [ + "text", + "image" + ] + }, "ModelConfig": { "description": "Pairs a provider and model together for a specific operation.", "type": "object", @@ -643,10 +651,14 @@ "type": "string" }, "models": { - "description": "URL template for fetching the model list; may contain `{{VAR}}`\nplaceholders.", - "type": [ - "string", - "null" + "description": "Model source for this provider. Can be either a URL template for\nfetching the model list, or a static list of model entries.", + "anyOf": [ + { + "$ref": "#/$defs/ProviderModels" + }, + { + "type": "null" + } ] }, "provider_type": { @@ -688,6 +700,22 @@ "url" ] }, + "ProviderModels": { + "description": "Model source for a provider.\n\nThis can be either a URL template for fetching the model list, or a\nstatic list of model entries defined inline.", + "anyOf": [ + { + "description": "URL template for fetching the model list.", + "type": "string" + }, + { + "description": "Static list of model entries.", + "type": "array", + "items": { + "$ref": "#/$defs/StaticModelEntry" + } + } + ] + }, "ProviderResponseType": { "description": "Wire protocol a provider uses for chat completions.", "type": "string", @@ -836,6 +864,73 @@ "suppress_errors" ] }, + "StaticModelEntry": { + "description": "A static model entry for inline provider configuration.\n\nThis allows defining model capabilities directly in `forge.toml` without\nrequiring a URL-based model discovery.", + "type": "object", + "properties": { + "context_length": { + "description": "Maximum context window size in tokens.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "description": { + "description": "Description of the model.", + "type": [ + "string", + "null" + ] + }, + "id": { + "description": "Unique model identifier (e.g. `\"gpt-4\"`).", + "type": "string" + }, + "input_modalities": { + "description": "Input modalities supported by the model (defaults to text-only).", + "type": "array", + "default": [ + "text" + ], + "items": { + "$ref": "#/$defs/InputModality" + } + }, + "name": { + "description": "Human-readable model name.", + "type": [ + "string", + "null" + ] + }, + "supports_parallel_tool_calls": { + "description": "Whether the model supports parallel tool calls.", + "type": [ + "boolean", + "null" + ] + }, + "supports_reasoning": { + "description": "Whether the model supports reasoning.", + "type": [ + "boolean", + "null" + ] + }, + "tools_supported": { + "description": "Whether the model supports tool calls.", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "id" + ] + }, "TlsBackend": { "description": "TLS backend option.", "type": "string", diff --git a/plans/2026-04-17-static-model-list-support-v1.md b/plans/2026-04-17-static-model-list-support-v1.md new file mode 100644 index 0000000000..b05a487865 --- /dev/null +++ b/plans/2026-04-17-static-model-list-support-v1.md @@ -0,0 +1,215 @@ +# Static Model List Support for forge.toml Configuration + +## Objective + +Enable users to define static model lists directly in `forge.toml` provider entries, in addition to the existing URL-based model discovery. This allows offline or air-gapped deployments to specify models without requiring API calls to fetch model lists. + +## Project Structure Summary + +The implementation spans three key areas: + +1. **Schema Layer** (`forge.schema.json`): JSON Schema definition for configuration validation +2. **Config Layer** (`crates/forge_config/src/config.rs`): Rust struct definitions with Serde deserialization +3. **Repository Layer** (`crates/forge_repo/src/provider/provider_repo.rs`): Internal model handling with `Models` enum + +## Relevant Files + +| File | Purpose | Current State | +|------|---------|---------------| +| `forge.schema.json:613-689` | `ProviderEntry` schema | `models` accepts only `string \| null` | +| `crates/forge_config/src/config.rs:69-99` | `ProviderEntry` struct | `models: Option` | +| `crates/forge_repo/src/provider/provider_repo.rs:13-21` | Internal `Models` enum | Already supports `Url` and `Hardcoded` | +| `crates/forge_domain/src/model.rs:23-38` | Domain `Model` struct | Defines model fields | +| `crates/forge_repo/src/provider/provider.json` | Embedded provider configs | Uses hardcoded model arrays | + +## Implementation Plan + +### Phase 1: Schema Updates + +- [x] **Task 1.1**: Update `forge.schema.json` to define a new `ModelEntry` schema + + - Define `ModelEntry` object with fields matching `forge_domain::Model`: + - `id` (string, required): Model identifier + - `name` (string, optional): Human-readable model name + - `description` (string, optional): Model description + - `context_length` (integer, optional): Maximum context window + - `tools_supported` (boolean, optional): Tool use capability + - `supports_parallel_tool_calls` (boolean, optional): Parallel tool calls support + - `supports_reasoning` (boolean, optional): Reasoning support + - `input_modalities` (array of strings, optional): Supported input types (`["text"]`, `["text", "image"]`) + + - Update `ProviderEntry.models` field to accept `oneOf`: + - String (URL template for fetching models) + - Array of `ModelEntry` objects (static model list) + - Null (no model list) + +### Phase 2: Config Layer Updates + +- [x] **Task 2.1**: Create `StaticModelEntry` struct in `forge_config/src/config.rs` + + ```rust + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Dummy)] + pub struct StaticModelEntry { + pub id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, + // ... other optional fields + } + ``` + + - Follow existing patterns: use `#[serde(default, skip_serializing_if = "Option::is_none")]` for optional fields + - Use `derive_setters::Setters` for builder pattern support + - Add JSON Schema derive for schema generation + +- [x] **Task 2.2**: Create `ProviderModels` enum with untagged deserialization + + ```rust + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] + #[serde(untagged)] + pub enum ProviderModels { + Url(String), + Static(Vec), + } + ``` + + - Use `#[serde(untagged)]` for seamless backward compatibility with string URLs + - Keep serialization compatible with both formats + +- [x] **Task 2.3**: Update `ProviderEntry` struct to use new enum + + ```rust + pub struct ProviderEntry { + // ... existing fields ... + #[serde(default, skip_serializing_if = "Option::is_none")] + pub models: Option, + } + ``` + +### Phase 3: Repository Layer Updates + +- [x] **Task 3.1**: Update `From` conversion in `provider_repo.rs` + + - Modify line 158 to handle both `ProviderModels::Url` and `ProviderModels::Static` variants + - Map `Static` variant to existing `Models::Hardcoded` enum variant + - Ensure proper conversion of `StaticModelEntry` to `forge_app::domain::Model` + +- [x] **Task 3.2**: Verify existing `From<&ProviderConfig>` conversion (lines 165-192) works unchanged + + - Internal `Models` enum already supports both variants + - Conversion logic handles `Models::Hardcoded` correctly + +### Phase 4: Validation + +- [x] **Task 4.1**: Add runtime validation for static model entries + + - Validate `id` field is non-empty + - Validate `context_length` is positive if provided + - Validate `input_modalities` contains valid values (`"text"`, `"image"`) + +- [x] **Task 4.2**: Consider adding validation at config load time + + - Implement `Validate` trait or custom deserialization logic + - Provide clear error messages for invalid configurations + +### Phase 5: Testing Strategy + +- [x] **Task 5.1**: Unit tests for `ProviderModels` deserialization + + ```rust + #[test] + fn test_provider_models_url() { + let json = r#""https://api.example.com/models""#; + let models: ProviderModels = serde_json::from_str(json).unwrap(); + assert!(matches!(models, ProviderModels::Url(_))); + } + + #[test] + fn test_provider_models_static() { + let json = r#"[{"id": "gpt-4"}]"#; + let models: ProviderModels = serde_json::from_str(json).unwrap(); + assert!(matches!(models, ProviderModels::Static(_))); + } + ``` + +- [x] **Task 5.2**: Integration tests for full provider config parsing + + - Test inline provider with URL-based models + - Test inline provider with static model list + - Test mixed providers in single `ForgeConfig` + +- [x] **Task 5.3**: Round-trip serialization tests + + - Verify static model lists serialize back to valid JSON + - Verify URL strings remain unchanged + +- [x] **Task 5.4**: Backward compatibility tests + + - Verify existing URL-only configurations continue to work + - Test with various URL template formats + +### Phase 6: Documentation + +- [x] **Task 6.1**: Update schema documentation + + - Document both `models` format options in schema descriptions + - Add examples showing URL vs static list usage + +- [x] **Task 6.2**: Consider updating user-facing docs (optional) + + - Explain when to use URL-based vs static model lists + - Provide example configurations + +## Verification Criteria + +- [x] Existing URL-based model configurations continue to work without modification +- [x] New static model list configurations parse correctly from TOML +- [x] Schema validation passes for both format options +- [x] All existing tests pass (`cargo insta test --accept`) +- [x] Code compiles without warnings (`cargo check`) +- [x] Round-trip serialization preserves model data accurately + +## Potential Risks and Mitigations + +1. **Risk**: Breaking change for existing configurations + - **Mitigation**: Untagged enum ensures backward compatibility with string URLs + - **Mitigation**: Extensive testing of existing URL-based configs + +2. **Risk**: Schema validation conflicts between URL and object formats + - **Mitigation**: Use `oneOf` in JSON Schema with clear type discrimination + - **Mitigation**: Rust untagged enum handles both cases automatically + +3. **Risk**: Inconsistent model field validation + - **Mitigation**: Align static model entry schema with domain `Model` struct fields + - **Mitigation**: Add runtime validation for required fields + +4. **Risk**: Merge semantics with hardcoded vs URL models + - **Mitigation**: Existing merge logic in `provider_repo.rs` already handles `Models` enum + - **Mitigation**: Verify merge behavior with both model source types + +## Alternative Approaches + +1. **Separate Fields Approach**: Use `models_url` and `models_static` as separate fields + - **Trade-off**: More explicit but breaks compatibility + - **Trade-off**: Users must choose one or the other + +2. **Custom Deserializer Approach**: Keep string field but parse array syntax internally + - **Trade-off**: Simpler schema but less explicit + - **Trade-off**: Magic parsing could be confusing + +3. **Reuse Existing Domain Types**: Reference domain `Model` type directly in config + - **Trade-off**: DRY but couples config layer to domain layer + - **Trade-off**: May introduce unnecessary dependencies + +**Selected Approach**: Untagged enum with dedicated `StaticModelEntry` struct +- Preserves backward compatibility (URL strings deserialize directly) +- Clear separation of concerns (config vs domain) +- Follows existing codebase patterns (similar to `UrlParamVarConfig`) + +## Implementation Order + +1. Update `forge.schema.json` first (schema drives validation) +2. Implement `StaticModelEntry` and `ProviderModels` in `forge_config` +3. Update `ProviderEntry` struct +4. Update conversion logic in `provider_repo.rs` +5. Add tests +6. Verify with `cargo check` and `cargo insta test` From 4ac15167ef000b6bceb448ce2574fc69d6c9d5ba Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 06:24:45 +0000 Subject: [PATCH 2/2] [autofix.ci] apply automated fixes --- crates/forge_config/src/config.rs | 41 +++++++++++++++++-------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/crates/forge_config/src/config.rs b/crates/forge_config/src/config.rs index 393bc8eaec..158f942a9e 100644 --- a/crates/forge_config/src/config.rs +++ b/crates/forge_config/src/config.rs @@ -121,10 +121,7 @@ pub enum ProviderModels { impl fake::Dummy for ProviderModels { fn dummy_with_rng(_: &fake::Faker, rng: &mut R) -> Self { // Generate a static list of models for testing - Self::Static(vec![StaticModelEntry::dummy_with_rng( - &fake::Faker, - rng, - )]) + Self::Static(vec![StaticModelEntry::dummy_with_rng(&fake::Faker, rng)]) } } @@ -441,7 +438,9 @@ mod tests { fn test_provider_models_url() { let json = r#""https://api.example.com/models""#; let models: ProviderModels = serde_json::from_str(json).unwrap(); - assert!(matches!(models, ProviderModels::Url(url) if url == "https://api.example.com/models")); + assert!( + matches!(models, ProviderModels::Url(url) if url == "https://api.example.com/models") + ); } #[test] @@ -482,18 +481,16 @@ mod tests { #[test] fn test_provider_models_round_trip() { - let original = ProviderModels::Static(vec![ - StaticModelEntry { - id: "qwen3-35b".to_string(), - name: Some("Qwen 3.5 35B".to_string()), - description: Some("Local reasoning model".to_string()), - context_length: Some(262144), - tools_supported: Some(true), - supports_parallel_tool_calls: Some(true), - supports_reasoning: Some(true), - input_modalities: vec![InputModality::Text], - }, - ]); + let original = ProviderModels::Static(vec![StaticModelEntry { + id: "qwen3-35b".to_string(), + name: Some("Qwen 3.5 35B".to_string()), + description: Some("Local reasoning model".to_string()), + context_length: Some(262144), + tools_supported: Some(true), + supports_parallel_tool_calls: Some(true), + supports_reasoning: Some(true), + input_modalities: vec![InputModality::Text], + }]); let json = serde_json::to_string(&original).unwrap(); let parsed: ProviderModels = serde_json::from_str(&json).unwrap(); @@ -503,8 +500,14 @@ mod tests { assert_eq!(parsed_entries.len(), 1); assert_eq!(parsed_entries[0].id, "qwen3-35b"); assert_eq!(parsed_entries[0].name, orig_entries[0].name); - assert_eq!(parsed_entries[0].context_length, orig_entries[0].context_length); - assert_eq!(parsed_entries[0].tools_supported, orig_entries[0].tools_supported); + assert_eq!( + parsed_entries[0].context_length, + orig_entries[0].context_length + ); + assert_eq!( + parsed_entries[0].tools_supported, + orig_entries[0].tools_supported + ); } other => panic!("Expected Static variants, got {:?}", other), }