Skip to content

feat: support external model and pricing configuration from TOML#140

Merged
mike1858 merged 6 commits intoPiebald-AI:mainfrom
jimyag:feature/external-models-config
Apr 20, 2026
Merged

feat: support external model and pricing configuration from TOML#140
mike1858 merged 6 commits intoPiebald-AI:mainfrom
jimyag:feature/external-models-config

Conversation

@jimyag
Copy link
Copy Markdown
Contributor

@jimyag jimyag commented Apr 8, 2026

This PR adds support for loading custom models and pricing mappings from the ~/.splitrail.toml configuration file.

Changes:

  • Refactored src/models.rs to support dynamic model registration using parking_lot::RwLock and OnceLock.
  • Updated pricing structures to use Vec instead of static slices for serialization support.
  • Added models and aliases fields to the Config struct in src/config.rs.
  • Initialized external models at the beginning of main in src/main.rs.
  • Added unit tests to verify TOML parsing and registry merging.

Configuration Example:

Users can now add custom models to their .splitrail.toml:

[models."my-custom-model"]
pricing = { Flat = { input_per_1m = 10.0, output_per_1m = 20.0 } }
caching = "None"
is_estimated = true

[aliases]
"super-model" = "my-custom-model"

Summary by CodeRabbit

  • New Features

    • Configurable custom models with full pricing and model aliases for shortcuts.
    • Custom models and aliases are registered at startup so all CLI commands use them immediately.
  • Tests

    • Added tests covering custom model/alias deserialization and registry merging.
  • UX

    • Config display now shows counts for custom models and aliases only when present.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 8, 2026

Warning

Rate limit exceeded

@jimyag has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 10 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 19 minutes and 10 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 600399c6-9075-4896-95c1-6e9239eeec5d

📥 Commits

Reviewing files that changed from the base of the PR and between 43eacfe and 7a57679.

📒 Files selected for processing (5)
  • src/config.rs
  • src/main.rs
  • src/models.rs
  • src/upload.rs
  • src/upload/tests.rs
📝 Walkthrough

Walkthrough

Config accepts user-provided models and aliases; main initializes a runtime model registry early with those values; compile-time model maps replaced by a write-locked global registry; model/pricing types now own tier data and are Serde (de)serializable; lookups return owned ModelInfo.

Changes

Cohort / File(s) Summary
Configuration Structure
src/config.rs
Added #[serde(default)] pub models: HashMap<String, ModelInfo> and pub aliases: HashMap<String, String> to Config; initialize maps in Config::default(); show_config() prints custom counts conditionally; added test_config_with_custom_models.
CLI Startup / Init
src/main.rs
Call models::init_external_models(config.models.clone(), config.aliases.clone()) immediately after loading config to populate the runtime registry before further initialization and command dispatch.
Runtime Registry & Model Types
src/models.rs
Replaced compile-time phf maps with a global OnceLock<RwLock<Registry>> and AtomicBool guard; added pub fn init_external_models(...) to merge external models/aliases once; pricing/caching/model structs now derive Serialize, Deserialize and store tiers in owned Vec<T>; get_model_info now returns Option<ModelInfo> (owned); added test_registry_merging and removed several prior static lookup tests.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as main()
    participant Config as Config Loader
    participant Init as models::init_external_models()
    participant Registry as Global Registry (OnceLock + RwLock)

    CLI->>Config: load configuration
    Config-->>CLI: return Config { models, aliases, ... }
    CLI->>Init: init_external_models(models, aliases)
    Init->>Registry: acquire write lock
    Registry->>Registry: merge models & aliases (guarded by AtomicBool)
    Registry-->>Init: release lock
    Init-->>CLI: initialization complete
    CLI->>CLI: continue startup and command dispatch
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I nudged the maps from stone to spring,

Models learned to live and cling.
Vecs replace slices, fresh and bright,
Registry wakes at morning light.
A tiny hop — the system's right!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective: adding support for loading custom models and pricing from TOML configuration files, which is the primary focus of the changes across all three modified files.
Docstring Coverage ✅ Passed Docstring coverage is 95.24% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/models.rs (1)

91-108: Consider documenting or enforcing single initialization.

The function comment says "should be called once at startup," but nothing prevents multiple calls. Each call will:

  1. Initialize defaults (only first time via get_or_init)
  2. Merge/overwrite entries (every time)

This is technically correct for the current use case, but if init_external_models is accidentally called multiple times (e.g., in tests or future refactoring), entries accumulate rather than being replaced entirely.

If this is intentional (allowing incremental additions), the doc comment should clarify. If single-init is expected, consider using a flag to warn or no-op on subsequent calls.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/models.rs` around lines 91 - 108, The init_external_models function
currently merges external_models and external_aliases into REGISTRY on every
call (using REGISTRY.get_or_init and then writing into
registry.index/registry.aliases), which contradicts the "should be called once"
doc; either document that incremental merges are intentional or enforce single
initialization by adding a guard: detect if REGISTRY was already initialized or
if registry.index/registry.aliases are non-empty and then either return/no-op or
log/warn and refuse to overwrite; update the comment to state behavior (merge vs
single-init) and modify init_external_models to check REGISTRY.get_or_init /
Registry state before inserting (using REGISTRY, init_external_models, Registry,
index, aliases, and get_or_init as anchors).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/config.rs`:
- Around line 298-325: The test test_model_registry_merging mutates the global
REGISTRY via init_external_models which can leak state across parallel tests;
make the test deterministic by either marking it serial (add #[serial] from
serial_test above fn test_model_registry_merging), or ensure cleanup by calling
a test-only reset function that clears external entries from REGISTRY (add a
#[cfg(test)] fn clear_external_models() that removes entries added by
init_external_models and call it in a teardown), or move the test into an
integration test file so it runs in its own process; reference
init_external_models, REGISTRY, get_model_info, and test_model_registry_merging
when applying the chosen fix.

---

Nitpick comments:
In `@src/models.rs`:
- Around line 91-108: The init_external_models function currently merges
external_models and external_aliases into REGISTRY on every call (using
REGISTRY.get_or_init and then writing into registry.index/registry.aliases),
which contradicts the "should be called once" doc; either document that
incremental merges are intentional or enforce single initialization by adding a
guard: detect if REGISTRY was already initialized or if
registry.index/registry.aliases are non-empty and then either return/no-op or
log/warn and refuse to overwrite; update the comment to state behavior (merge vs
single-init) and modify init_external_models to check REGISTRY.get_or_init /
Registry state before inserting (using REGISTRY, init_external_models, Registry,
index, aliases, and get_or_init as anchors).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0cf09d5e-f935-448a-85c4-7fe15398214c

📥 Commits

Reviewing files that changed from the base of the PR and between f5ef1f4 and 6cbc2a7.

📒 Files selected for processing (3)
  • src/config.rs
  • src/main.rs
  • src/models.rs

Comment thread src/config.rs Outdated
@jimyag jimyag force-pushed the feature/external-models-config branch from 6cbc2a7 to eeddb84 Compare April 8, 2026 06:22
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/config.rs`:
- Around line 285-295: The test references PricingStructure::Flat but
PricingStructure isn't imported into the test module; add an import for the enum
(e.g. use crate::models::PricingStructure;) at the top of the test module so the
match on custom_model.pricing compiles, ensuring the test can match
PricingStructure::Flat and access input_per_1m/output_per_1m.

In `@src/models.rs`:
- Around line 1563-1566: The match is trying to move a non-Copy enum out of a
borrowed ModelInfo (`info`); change the match to pattern-match on a reference to
`pricing` (e.g., `match &info.pricing`) so you destructure
`PricingStructure::Flat { input_per_1m, .. }` by reference and avoid moving;
keep the same assertion on `input_per_1m` after adjusting the pattern to bind
the referenced field.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fad62d8f-d06f-494b-9739-f7be2ff8e288

📥 Commits

Reviewing files that changed from the base of the PR and between 6cbc2a7 and eeddb84.

📒 Files selected for processing (3)
  • src/config.rs
  • src/main.rs
  • src/models.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main.rs

Comment thread src/config.rs
Comment thread src/models.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/models.rs (1)

115-131: Consider documenting the single-threaded startup requirement.

The initialization pattern has a brief window between the atomic flag swap and the merge completion where concurrent readers could see incomplete data. While this is mitigated by calling init_external_models at the start of main() before spawning any tasks (as seen in the context), the function's doc comment could be more explicit about this requirement.

📝 Suggested documentation improvement
 /// Initialize the model registry with external configuration.
-/// This should be called once at startup.
+/// This should be called once at startup, before any concurrent access to
+/// model lookups. Calling from `main()` before spawning tasks is recommended.
 pub fn init_external_models(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/models.rs` around lines 115 - 131, Update the doc comment for
init_external_models to explicitly state it must be called once during
single-threaded startup (e.g., in main() before spawning threads/tasks) because
EXTERNAL_MODELS_INITIALIZED is set before merging into REGISTRY and there is a
brief window where concurrent readers could observe incomplete state; reference
init_external_models, EXTERNAL_MODELS_INITIALIZED, REGISTRY, and Registry::merge
in the comment so callers know to call it prior to concurrent access.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/models.rs`:
- Around line 115-131: Update the doc comment for init_external_models to
explicitly state it must be called once during single-threaded startup (e.g., in
main() before spawning threads/tasks) because EXTERNAL_MODELS_INITIALIZED is set
before merging into REGISTRY and there is a brief window where concurrent
readers could observe incomplete state; reference init_external_models,
EXTERNAL_MODELS_INITIALIZED, REGISTRY, and Registry::merge in the comment so
callers know to call it prior to concurrent access.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 589c8fe9-1678-4254-8597-c3fc99049638

📥 Commits

Reviewing files that changed from the base of the PR and between eeddb84 and 32937f1.

📒 Files selected for processing (2)
  • src/config.rs
  • src/models.rs

@mike1858
Copy link
Copy Markdown
Member

@copilot resolve the merge conflicts in this pull request

1 similar comment
@jimyag
Copy link
Copy Markdown
Contributor Author

jimyag commented Apr 20, 2026

@copilot resolve the merge conflicts in this pull request

Bring the Copilot CLI analyzer, docs, and test updates into this branch while preserving the external model configuration work and resolving the src/models.rs conflicts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: jimyag <git@jimyag.com>
@mike1858
Copy link
Copy Markdown
Member

@copilot Resolve the new conflicts. I accidentally merged my own PR which adds some new models, directly conflicting with this PR.

Bring the GPT-4.5 and GPT-5.4-nano pricing updates into this branch while preserving the external model registry and prior merge resolution.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: jimyag <git@jimyag.com>
coderabbitai[bot]
coderabbitai Bot previously requested changes Apr 20, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/models.rs (1)

1326-1335: Hot-path clones on every cost calculation.

lookup_model now clones ModelInfo on every call, and calculate_total_cost triggers three lookups (input + output + cache) per usage record. For tiered models this allocates/drops Vec<PricingTier> and Vec<CachingTier> each time, which is a regression vs. the previous static phf maps. On large session aggregations this can be noticeable.

Consider storing Arc<ModelInfo> in the registry (and returning Arc<ModelInfo> from get_model_info) so lookups are cheap ref-count bumps without heap allocation, or have calculate_total_cost perform a single lookup and pass the &ModelInfo into input/output/cache helpers.

♻️ Sketch
 struct Registry {
-    index: HashMap<String, ModelInfo>,
+    index: HashMap<String, Arc<ModelInfo>>,
     aliases: HashMap<String, String>,
 }
...
-fn lookup_model(name: &str) -> Option<ModelInfo> {
+fn lookup_model(name: &str) -> Option<Arc<ModelInfo>> {
     let registry = get_registry_lock().read();
     if let Some(model_info) = registry.index.get(name) {
-        return Some(model_info.clone());
+        return Some(Arc::clone(model_info));
     }
     if let Some(canonical_name) = registry.aliases.get(name) {
-        return registry.index.get(canonical_name).cloned();
+        return registry.index.get(canonical_name).map(Arc::clone);
     }
     None
 }

And/or refactor calculate_total_cost to do one lookup and dispatch:

pub fn calculate_total_cost(model_name: &str, input: u64, output: u64, cc: u64, cr: u64) -> f64 {
    let Some(info) = get_model_info(model_name) else { /* warn + return 0.0 */ };
    input_cost(&info, input) + output_cost(&info, output) + cache_cost(&info, cc, cr)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/models.rs` around lines 1326 - 1335, lookup_model currently clones
ModelInfo on every call (via registry.index.get(name).cloned()), causing
hot-path allocations when calculate_total_cost calls it multiple times; change
the registry to store Arc<ModelInfo> (update the registry.index and
registry.aliases types) and make lookup_model return Arc<ModelInfo> (or change
get_model_info to return Arc<ModelInfo>) so lookups are just ref-count bumps, or
alternatively refactor calculate_total_cost to perform a single lookup (call
lookup_model/get_model_info once) and pass a &ModelInfo into
input_cost/output_cost/cache_cost helpers to avoid repeated clones of
Vec<PricingTier>/Vec<CachingTier>; update any call sites and signatures
(lookup_model, get_model_info, calculate_total_cost, input_cost, output_cost,
cache_cost) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/models.rs`:
- Around line 117-131: The flag EXTERNAL_MODELS_INITIALIZED is being set at the
start of init_external_models, so any panic or partial init before calling
registry.merge will leave the flag true and silently skip subsequent calls;
change the flow to perform the registry write and call Registry::merge first
(using REGISTRY and its RwLock), and only after a successful merge set
EXTERNAL_MODELS_INITIALIZED (or remove the flag entirely and rely on
OnceLock/one-call contract); also update warn_once to include more context
(function name or note that later [models]/[aliases] were ignored) so repeated
calls don’t silently drop configuration — key symbols: init_external_models,
EXTERNAL_MODELS_INITIALIZED, REGISTRY, RwLock, Registry::merge, warn_once.

---

Nitpick comments:
In `@src/models.rs`:
- Around line 1326-1335: lookup_model currently clones ModelInfo on every call
(via registry.index.get(name).cloned()), causing hot-path allocations when
calculate_total_cost calls it multiple times; change the registry to store
Arc<ModelInfo> (update the registry.index and registry.aliases types) and make
lookup_model return Arc<ModelInfo> (or change get_model_info to return
Arc<ModelInfo>) so lookups are just ref-count bumps, or alternatively refactor
calculate_total_cost to perform a single lookup (call
lookup_model/get_model_info once) and pass a &ModelInfo into
input_cost/output_cost/cache_cost helpers to avoid repeated clones of
Vec<PricingTier>/Vec<CachingTier>; update any call sites and signatures
(lookup_model, get_model_info, calculate_total_cost, input_cost, output_cost,
cache_cost) accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5b36357a-c9dc-4e55-b2ab-f3f2dc585a8f

📥 Commits

Reviewing files that changed from the base of the PR and between 32937f1 and 4bbd69c.

📒 Files selected for processing (2)
  • src/main.rs
  • src/models.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main.rs

Comment thread src/models.rs
jimyag and others added 2 commits April 20, 2026 10:36
Use an Arc-backed model registry so repeated external config merges are applied without hot-path clones, and stabilize the upload test helpers and test-only HTTP client lifecycle uncovered during verification.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: jimyag <git@jimyag.com>
Resolve the new review findings by validating external tier configs, following alias chains transitively, restoring the missing doubao canonical model, and tightening upload TLS and header parsing behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: jimyag <git@jimyag.com>
@jimyag
Copy link
Copy Markdown
Contributor Author

jimyag commented Apr 20, 2026

@coderabbitai review again

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

@jimyag Sure! I'll kick off a fresh review of PR #140 now.

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Member

@mike1858 mike1858 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@mike1858 mike1858 merged commit d6f7739 into Piebald-AI:main Apr 20, 2026
6 checks passed
@jimyag jimyag deleted the feature/external-models-config branch April 20, 2026 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants